From b2d35c528654046eaed5d5d32099de32384400ae Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Tue, 15 Oct 2024 17:46:52 +0300 Subject: [PATCH 001/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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/244] 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", From 87b77e3e5f763b1b51c0dbfa9e7bb026a0ee57af Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 7 May 2025 13:47:23 +0300 Subject: [PATCH 106/244] fix(client): add type annotations (#2949) Fix type parameter for transformTuplesReply in CONFIG_GET and HGETALL commands fixes #2933 --- packages/client/lib/commands/CONFIG_GET.ts | 2 +- packages/client/lib/commands/HGETALL.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index 54fa997bf61..e8339c4d9a0 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -10,7 +10,7 @@ export default { parser.pushVariadic(parameters); }, transformReply: { - 2: transformTuplesReply, + 2: transformTuplesReply, 3: undefined as unknown as () => MapReply } } as const satisfies Command; diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts index a2c3011c4c2..8d53669cdd4 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -11,7 +11,7 @@ export default { }, TRANSFORM_LEGACY_REPLY: true, transformReply: { - 2: transformTuplesReply, + 2: transformTuplesReply, 3: undefined as unknown as () => MapReply } } as const satisfies Command; From bc4b2101ee20aa916883ecd85fbb24f1eb496df5 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Wed, 7 May 2025 16:10:03 +0300 Subject: [PATCH 107/244] Export CommandParser from client index file and fix doc (#2945) * Export CommandParser from client index file * Tidy up long export line in client index file Wrap and sort entries. * Adapt and fix wrong examples in programmability doc --- docs/programmability.md | 10 ++++------ packages/client/index.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/programmability.md b/docs/programmability.md index 56eb048ca0c..c5917c2387d 100644 --- a/docs/programmability.md +++ b/docs/programmability.md @@ -25,9 +25,8 @@ 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 { CommandParser } from '@redis/client/lib/client/parser'; -import { NumberReply } from '@redis/client/lib/RESP/types'; -import { createClient, RedisArgument } from 'redis'; +import { CommandParser, createClient, RedisArgument } from '@redis/client'; +import { NumberReply } from '@redis/client/dist/lib/RESP/types.js'; const client = createClient({ functions: { @@ -58,9 +57,8 @@ await client.library.add('key', '2'); // 3 The following is an end-to-end example of the prior concept. ```typescript -import { CommandParser } from '@redis/client/lib/client/parser'; -import { NumberReply } from '@redis/client/lib/RESP/types'; -import { createClient, defineScript, RedisArgument } from 'redis'; +import { CommandParser, createClient, defineScript, RedisArgument } from '@redis/client'; +import { NumberReply } from '@redis/client/dist/lib/RESP/types.js'; const client = createClient({ scripts: { diff --git a/packages/client/index.ts b/packages/client/index.ts index e426badf126..1f05bc30341 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -1,4 +1,12 @@ -export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/*, CommandPolicies*/, RedisArgument } from './lib/RESP/types'; +export { + /* CommandPolicies, */ + RedisArgument, + RedisFunctions, + RedisModules, + RedisScripts, + RespVersions, + TypeMapping, +} from './lib/RESP/types'; export { RESP_TYPES } from './lib/RESP/decoder'; export { VerbatimString } from './lib/RESP/verbatim-string'; export { defineScript } from './lib/lua-script'; @@ -7,6 +15,7 @@ export * from './lib/errors'; import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client'; export { RedisClientOptions, RedisClientType }; export const createClient = RedisClient.create; +export { CommandParser } from './lib/client/parser'; import { RedisClientPool, RedisPoolOptions, RedisClientPoolType } from './lib/client/pool'; export { RedisClientPoolType, RedisPoolOptions }; From 7b737821b23a0180bfb5a3b010c2612472b15edc Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 7 May 2025 16:10:35 +0300 Subject: [PATCH 108/244] fix: fix various command import issues (#2944) * fix: fix various command import issues there was some sort of a circular dependency in /lib/commands/index.ts for various modules fixes #2937 fixes #2941 * remove redundant definition --- packages/bloom/lib/commands/bloom/INFO.ts | 2 +- packages/bloom/lib/commands/bloom/helpers.ts | 29 ++ packages/bloom/lib/commands/bloom/index.ts | 33 +- packages/client/lib/client/index.ts | 3 +- packages/json/lib/commands/ARRAPPEND.ts | 2 +- packages/json/lib/commands/ARRINDEX.ts | 2 +- packages/json/lib/commands/ARRINSERT.ts | 2 +- packages/json/lib/commands/ARRPOP.ts | 2 +- packages/json/lib/commands/GET.spec.ts | 6 + packages/json/lib/commands/GET.ts | 12 +- packages/json/lib/commands/MERGE.ts | 2 +- packages/json/lib/commands/MGET.ts | 2 +- packages/json/lib/commands/MSET.ts | 2 +- packages/json/lib/commands/SET.ts | 2 +- packages/json/lib/commands/STRAPPEND.ts | 2 +- packages/json/lib/commands/helpers.ts | 22 ++ packages/json/lib/commands/index.ts | 20 +- packages/time-series/lib/commands/ADD.spec.ts | 2 +- packages/time-series/lib/commands/ADD.ts | 2 +- .../time-series/lib/commands/ALTER.spec.ts | 2 +- packages/time-series/lib/commands/ALTER.ts | 3 +- .../time-series/lib/commands/CREATE.spec.ts | 2 +- packages/time-series/lib/commands/CREATE.ts | 2 +- packages/time-series/lib/commands/DEL.ts | 2 +- packages/time-series/lib/commands/INCRBY.ts | 2 +- .../time-series/lib/commands/INFO.spec.ts | 2 +- packages/time-series/lib/commands/INFO.ts | 2 +- .../lib/commands/INFO_DEBUG.spec.ts | 2 +- packages/time-series/lib/commands/MADD.ts | 2 +- packages/time-series/lib/commands/MGET.ts | 2 +- .../lib/commands/MGET_SELECTED_LABELS.ts | 2 +- .../lib/commands/MGET_WITHLABELS.ts | 2 +- packages/time-series/lib/commands/MRANGE.ts | 2 +- .../lib/commands/MRANGE_GROUPBY.ts | 2 +- .../lib/commands/MRANGE_SELECTED_LABELS.ts | 2 +- .../MRANGE_SELECTED_LABELS_GROUPBY.ts | 2 +- .../lib/commands/MRANGE_WITHLABELS.ts | 2 +- .../lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 2 +- packages/time-series/lib/commands/RANGE.ts | 2 +- packages/time-series/lib/commands/helpers.ts | 306 +++++++++++++++++ .../time-series/lib/commands/index.spec.ts | 2 +- packages/time-series/lib/commands/index.ts | 309 +----------------- 42 files changed, 416 insertions(+), 391 deletions(-) create mode 100644 packages/bloom/lib/commands/bloom/helpers.ts create mode 100644 packages/json/lib/commands/helpers.ts create mode 100644 packages/time-series/lib/commands/helpers.ts diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index b715bf57738..7074885a41e 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -1,6 +1,6 @@ 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 '.'; +import { transformInfoV2Reply } from './helpers'; export type BfInfoReplyMap = TuplesToMapReply<[ [SimpleStringReply<'Capacity'>, NumberReply], diff --git a/packages/bloom/lib/commands/bloom/helpers.ts b/packages/bloom/lib/commands/bloom/helpers.ts new file mode 100644 index 00000000000..f5b39c71aa8 --- /dev/null +++ b/packages/bloom/lib/commands/bloom/helpers.ts @@ -0,0 +1,29 @@ +import { RESP_TYPES, TypeMapping } from "@redis/client"; + +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/bloom/index.ts b/packages/bloom/lib/commands/bloom/index.ts index a93f79c9c56..d49ac63b2ea 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 } from '@redis/client/dist/lib/RESP/types'; import ADD from './ADD'; import CARD from './CARD'; @@ -10,7 +10,8 @@ import MADD from './MADD'; import MEXISTS from './MEXISTS'; import RESERVE from './RESERVE'; import SCANDUMP from './SCANDUMP'; -import { RESP_TYPES } from '@redis/client'; + +export * from './helpers'; export default { ADD, @@ -34,31 +35,3 @@ export default { 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/client/lib/client/index.ts b/packages/client/lib/client/index.ts index f48e03d0c19..f9e95025c6a 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -704,7 +704,8 @@ export default class RedisClient< const reply = await this.sendCommand(parser.redisArgs, commandOptions); if (transformReply) { - return transformReply(reply, parser.preserve, commandOptions?.typeMapping); + const res = transformReply(reply, parser.preserve, commandOptions?.typeMapping); + return res } return reply; diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index 539eb91a297..d2283b128e3 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,5 +1,5 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 23f010b1f0b..6ffcf9f5f0e 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; export interface JsonArrIndexOptions { range?: { diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index eb1d7c882f2..e64e0b18559 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 5f1489c3bd4..30ed34c37be 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,7 +1,7 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; 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 '.'; +import { transformRedisJsonNullReply } from './helpers'; export interface RedisArrPopOptions { path: RedisArgument; diff --git a/packages/json/lib/commands/GET.spec.ts b/packages/json/lib/commands/GET.spec.ts index 0741de316e1..6b4f44871cb 100644 --- a/packages/json/lib/commands/GET.spec.ts +++ b/packages/json/lib/commands/GET.spec.ts @@ -34,5 +34,11 @@ describe('JSON.GET', () => { await client.json.get('key'), null ); + + await client.json.set('noderedis:users:1', '$', { name: 'Alice', age: 32, }) + const res = await client.json.get('noderedis:users:1'); + assert.equal(typeof res, 'object') + assert.deepEqual(res, { name: 'Alice', age: 32, }) + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index d43d7464c50..6705ac534bc 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,7 +1,7 @@ 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 { transformRedisJsonNullReply } from '.'; +import { transformRedisJsonNullReply } from './helpers'; export interface JsonGetOptions { path?: RedisVariadicArgument; @@ -9,12 +9,16 @@ export interface JsonGetOptions { export default { IS_READ_ONLY: false, - parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonGetOptions) { + parseCommand( + parser: CommandParser, + key: RedisArgument, + options?: JsonGetOptions + ) { parser.push('JSON.GET'); parser.pushKey(key); if (options?.path !== undefined) { - parser.pushVariadic(options.path) + parser.pushVariadic(options.path); } }, transformReply: transformRedisJsonNullReply -} as const satisfies Command; +} as const satisfies Command; \ No newline at end of file diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 0cb8131a68c..3c93913f91a 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index 447de064d2b..d0fc0a99089 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformRedisJsonNullReply } from '.'; +import { transformRedisJsonNullReply } from './helpers'; export default { IS_READ_ONLY: true, diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index cb0bea26ddd..2dfab142493 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; export interface JsonMSetItem { key: RedisArgument; diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index 75d7099acfb..27da2ec64ee 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,6 +1,6 @@ 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 '.'; +import { RedisJSON, transformRedisJsonArgument } from './helpers'; export interface JsonSetOptions { condition?: 'NX' | 'XX'; diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index 45d503856ac..3c0e5767549 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,6 +1,6 @@ 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 '.'; +import { transformRedisJsonArgument } from './helpers'; export interface JsonStrAppendOptions { path?: RedisArgument; diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts new file mode 100644 index 00000000000..26ff12f6834 --- /dev/null +++ b/packages/json/lib/commands/helpers.ts @@ -0,0 +1,22 @@ +import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformers"; +import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; + +export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { + console.log('transformRedisJsonNullReply', json) + return isNullReply(json) ? json : transformRedisJsonReply(json); +} + +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); +} + +export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { + const res = JSON.parse((json as unknown as UnwrapReply).toString()); + console.log('transformRedisJsonReply', json, res) + return res; +} diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index 2724ff2565c..a9e16bde757 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -1,4 +1,3 @@ -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 +22,8 @@ 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 * from './helpers'; export default { ARRAPPEND, @@ -82,19 +82,3 @@ export default { type: TYPE }; -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); -} - -export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { - return JSON.parse((json as unknown as UnwrapReply).toString()); -} - -export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - return isNullReply(json) ? json : transformRedisJsonReply(json); -} diff --git a/packages/time-series/lib/commands/ADD.spec.ts b/packages/time-series/lib/commands/ADD.spec.ts index 055d2246d8b..d66c85441a1 100644 --- a/packages/time-series/lib/commands/ADD.spec.ts +++ b/packages/time-series/lib/commands/ADD.spec.ts @@ -1,7 +1,7 @@ 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 { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from './helpers'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.ADD', () => { diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index e7626d227da..0f254339ff9 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -11,7 +11,7 @@ import { parseLabelsArgument, Timestamp, parseIgnoreArgument -} from '.'; +} from './helpers'; export interface TsIgnoreOptions { maxTimeDiff: number; diff --git a/packages/time-series/lib/commands/ALTER.spec.ts b/packages/time-series/lib/commands/ALTER.spec.ts index 560d9ffde2c..46b94c5863a 100644 --- a/packages/time-series/lib/commands/ALTER.spec.ts +++ b/packages/time-series/lib/commands/ALTER.spec.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALTER from './ALTER'; -import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.ALTER', () => { diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index f7f6948da71..29f99290a52 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,7 +1,8 @@ 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 '.'; +import { parseRetentionArgument, parseChunkSizeArgument, parseDuplicatePolicy, parseLabelsArgument, parseIgnoreArgument } from './helpers'; + export type TsAlterOptions = Pick; diff --git a/packages/time-series/lib/commands/CREATE.spec.ts b/packages/time-series/lib/commands/CREATE.spec.ts index 795b59b880d..4fbfabb6858 100644 --- a/packages/time-series/lib/commands/CREATE.spec.ts +++ b/packages/time-series/lib/commands/CREATE.spec.ts @@ -1,7 +1,7 @@ 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 { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from './helpers'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.CREATE', () => { diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index 39f35c06ed7..c499a752f23 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -10,7 +10,7 @@ import { Labels, parseLabelsArgument, parseIgnoreArgument -} from '.'; +} from './helpers'; import { TsIgnoreOptions } from './ADD'; export interface TsCreateOptions { diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index fc96c989b18..de9cadf88c9 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -1,5 +1,5 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { Timestamp, transformTimestampArgument } from '.'; +import { Timestamp, transformTimestampArgument } from './helpers'; import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types'; export default { diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index e62ec42690a..2365f716a83 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -1,6 +1,6 @@ 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 { Timestamp, transformTimestampArgument, parseRetentionArgument, parseChunkSizeArgument, Labels, parseLabelsArgument, parseIgnoreArgument } from './helpers'; import { TsIgnoreOptions } from './ADD'; export interface TsIncrByOptions { diff --git a/packages/time-series/lib/commands/INFO.spec.ts b/packages/time-series/lib/commands/INFO.spec.ts index 73b9d8dc930..994cb281915 100644 --- a/packages/time-series/lib/commands/INFO.spec.ts +++ b/packages/time-series/lib/commands/INFO.spec.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'node:assert'; -import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers'; import testUtils, { GLOBAL } from '../test-utils'; import INFO, { InfoReply } from './INFO'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index fe0e49e095a..62cc1108a80 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -1,6 +1,6 @@ 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 { TimeSeriesDuplicatePolicies } from "./helpers"; import { TimeSeriesAggregationType } from "./CREATERULE"; import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; diff --git a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts index 063b9126550..ff9d6aa3c72 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'node:assert'; -import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { TIME_SERIES_DUPLICATE_POLICIES } from './helpers'; import testUtils, { GLOBAL } from '../test-utils'; import { assertInfo } from './INFO.spec'; import INFO_DEBUG from './INFO_DEBUG'; diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index 5af94d6d497..b4c91a98384 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -1,5 +1,5 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { Timestamp, transformTimestampArgument } from '.'; +import { Timestamp, transformTimestampArgument } from './helpers'; import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface TsMAddSample { diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index fa4e3fc63d6..fd5e8c71b93 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -1,6 +1,6 @@ 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 { resp2MapToValue, resp3MapToValue, SampleRawReply, transformSampleReply } from './helpers'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface TsMGetOptions { diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts index e93b517f80a..d74d073c174 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -2,7 +2,7 @@ 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 { parseSelectedLabelsArguments } from './helpers'; import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; export default { diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index 38b8442db31..737e7236130 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -2,7 +2,7 @@ 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 '.'; +import { RawLabelValue, resp2MapToValue, resp3MapToValue, SampleRawReply, transformRESP2Labels, transformSampleReply } from './helpers'; export interface TsMGetWithLabelsOptions extends TsMGetOptions { SELECTED_LABELS?: RedisVariadicArgument; diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index 95fa5297bdd..3351b755499 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,7 +1,7 @@ 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 { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; 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 5ccd61b2a26..74279d00b6d 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -1,7 +1,7 @@ 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 { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; 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 643b57a67e7..75affc54aeb 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -1,7 +1,7 @@ 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 { parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from './helpers'; 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 c5cf1ef56c5..99429a9bb76 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -1,7 +1,7 @@ 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 { parseSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index 19641596a67..ef4864a0307 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,7 +1,7 @@ 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 { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; 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 ff0065e22b7..6552f6328e8 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -1,7 +1,7 @@ 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 { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2LabelsWithSources, transformSamplesReply } from './helpers'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index f7f808cecdb..44da30d81de 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -1,6 +1,6 @@ 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 { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from './helpers'; import { TimeSeriesAggregationType } from './CREATERULE'; import { Resp2Reply } from '@redis/client/dist/lib/RESP/types'; diff --git a/packages/time-series/lib/commands/helpers.ts b/packages/time-series/lib/commands/helpers.ts new file mode 100644 index 00000000000..3e277d0747d --- /dev/null +++ b/packages/time-series/lib/commands/helpers.ts @@ -0,0 +1,306 @@ +import { CommandParser } from "@redis/client/dist/lib/client/parser"; +import { TsIgnoreOptions } from "./ADD"; +import { ArrayReply, BlobStringReply, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, Resp2Reply, RespType, TuplesReply, TypeMapping, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; +import { RESP_TYPES } from "@redis/client"; +import { RedisVariadicArgument } from "@redis/client/dist/lib/commands/generic-transformers"; + +export function parseIgnoreArgument(parser: CommandParser, ignore?: TsIgnoreOptions) { + if (ignore !== undefined) { + parser.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString()); + } +} + +export function parseRetentionArgument(parser: CommandParser, retention?: number) { + if (retention !== undefined) { + parser.push('RETENTION', retention.toString()); + } +} + +export const TIME_SERIES_ENCODING = { + COMPRESSED: 'COMPRESSED', + UNCOMPRESSED: 'UNCOMPRESSED' +} as const; + +export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING]; + +export function parseEncodingArgument(parser: CommandParser, encoding?: TimeSeriesEncoding) { + if (encoding !== undefined) { + parser.push('ENCODING', encoding); + } +} + +export function parseChunkSizeArgument(parser: CommandParser, chunkSize?: number) { + if (chunkSize !== undefined) { + parser.push('CHUNK_SIZE', chunkSize.toString()); + } +} + +export const TIME_SERIES_DUPLICATE_POLICIES = { + BLOCK: 'BLOCK', + FIRST: 'FIRST', + LAST: 'LAST', + MIN: 'MIN', + MAX: 'MAX', + SUM: 'SUM' +} as const; + +export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES]; + +export function parseDuplicatePolicy(parser: CommandParser, duplicatePolicy?: TimeSeriesDuplicatePolicies) { + if (duplicatePolicy !== undefined) { + parser.push('DUPLICATE_POLICY', duplicatePolicy); + } +} + +export type Timestamp = number | Date | string; + +export function transformTimestampArgument(timestamp: Timestamp): string { + if (typeof timestamp === 'string') return timestamp; + + return ( + typeof timestamp === 'number' ? + timestamp : + timestamp.getTime() + ).toString(); +} + +export type Labels = { + [label: string]: string; +}; + +export function parseLabelsArgument(parser: CommandParser, labels?: Labels) { + if (labels) { + parser.push('LABELS'); + + for (const [label, value] of Object.entries(labels)) { + parser.push(label, value); + } + } +} + +export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>; + +export const transformSampleReply = { + 2(reply: Resp2Reply) { + const [ timestamp, value ] = reply as unknown as UnwrapReply; + return { + timestamp, + value: Number(value) // TODO: use double type mapping instead + }; + }, + 3(reply: SampleRawReply) { + const [ timestamp, value ] = reply as unknown as UnwrapReply; + return { + timestamp, + value + }; + } +}; + +export type SamplesRawReply = ArrayReply; + +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)); + } +}; + +// 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; + } + 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; + } + 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; + } + } +} + +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); + } + } 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) + ); + } + } else { + for (const [key, value] of Object.entries(reply)) { + (reply[key] as unknown as TRANSFORMED) = parseFunc(value as unknown as UnwrapReply); + } + } + return reply as never; +} + +export function parseSelectedLabelsArguments( + parser: CommandParser, + selectedLabels: RedisVariadicArgument +) { + parser.push('SELECTED_LABELS'); + parser.pushVariadic(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 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; + } + + 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); + + return { + labels: transformedLabels, + sources: transformedSources + }; +} + +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(','); + } + + const indexOfComma = unwrappedSources.indexOf(','); + if (indexOfComma === -1) { + return [unwrappedSources]; + } + + 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; + } + + const source = unwrappedSources.subarray( + previousComma, + indexOf + ); + sourcesArray.push(source); + previousComma = indexOf + 1; + } + + return sourcesArray; +} \ No newline at end of file diff --git a/packages/time-series/lib/commands/index.spec.ts b/packages/time-series/lib/commands/index.spec.ts index 5b28708152f..b565abea476 100644 --- a/packages/time-series/lib/commands/index.spec.ts +++ b/packages/time-series/lib/commands/index.spec.ts @@ -24,7 +24,7 @@ // TimeSeriesDuplicatePolicies, // pushLatestArgument, // TimeSeriesBucketTimestamp -// } from '.'; +// } from './helpers'; // describe('transformTimestampArgument', () => { // it('number', () => { diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts index f340861cb96..43bde4767bf 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -1,5 +1,4 @@ -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 ADD from './ADD'; import ALTER from './ALTER'; import CREATE from './CREATE'; import CREATERULE from './CREATERULE'; @@ -29,9 +28,9 @@ import MREVRANGE from './MREVRANGE'; import QUERYINDEX from './QUERYINDEX'; import RANGE from './RANGE'; import REVRANGE from './REVRANGE'; -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'; +import { RedisCommands } from '@redis/client/dist/lib/RESP/types'; + +export * from './helpers'; export default { ADD, @@ -96,303 +95,3 @@ export default { revRange: REVRANGE } as const satisfies RedisCommands; -export function parseIgnoreArgument(parser: CommandParser, ignore?: TsIgnoreOptions) { - if (ignore !== undefined) { - parser.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString()); - } -} - -export function parseRetentionArgument(parser: CommandParser, retention?: number) { - if (retention !== undefined) { - parser.push('RETENTION', retention.toString()); - } -} - -export const TIME_SERIES_ENCODING = { - COMPRESSED: 'COMPRESSED', - UNCOMPRESSED: 'UNCOMPRESSED' -} as const; - -export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING]; - -export function parseEncodingArgument(parser: CommandParser, encoding?: TimeSeriesEncoding) { - if (encoding !== undefined) { - parser.push('ENCODING', encoding); - } -} - -export function parseChunkSizeArgument(parser: CommandParser, chunkSize?: number) { - if (chunkSize !== undefined) { - parser.push('CHUNK_SIZE', chunkSize.toString()); - } -} - -export const TIME_SERIES_DUPLICATE_POLICIES = { - BLOCK: 'BLOCK', - FIRST: 'FIRST', - LAST: 'LAST', - MIN: 'MIN', - MAX: 'MAX', - SUM: 'SUM' -} as const; - -export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES]; - -export function parseDuplicatePolicy(parser: CommandParser, duplicatePolicy?: TimeSeriesDuplicatePolicies) { - if (duplicatePolicy !== undefined) { - parser.push('DUPLICATE_POLICY', duplicatePolicy); - } -} - -export type Timestamp = number | Date | string; - -export function transformTimestampArgument(timestamp: Timestamp): string { - if (typeof timestamp === 'string') return timestamp; - - return ( - typeof timestamp === 'number' ? - timestamp : - timestamp.getTime() - ).toString(); -} - -export type Labels = { - [label: string]: string; -}; - -export function parseLabelsArgument(parser: CommandParser, labels?: Labels) { - if (labels) { - parser.push('LABELS'); - - for (const [label, value] of Object.entries(labels)) { - parser.push(label, value); - } - } -} - -export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>; - -export const transformSampleReply = { - 2(reply: Resp2Reply) { - const [ timestamp, value ] = reply as unknown as UnwrapReply; - return { - timestamp, - value: Number(value) // TODO: use double type mapping instead - }; - }, - 3(reply: SampleRawReply) { - const [ timestamp, value ] = reply as unknown as UnwrapReply; - return { - timestamp, - value - }; - } -}; - -export type SamplesRawReply = ArrayReply; - -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)); - } -}; - -// 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; - } - 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; - } - 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; - } - } -} - -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); - } - } 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) - ); - } - } else { - for (const [key, value] of Object.entries(reply)) { - (reply[key] as unknown as TRANSFORMED) = parseFunc(value as unknown as UnwrapReply); - } - } - return reply as never; -} - -export function parseSelectedLabelsArguments( - parser: CommandParser, - selectedLabels: RedisVariadicArgument -) { - parser.push('SELECTED_LABELS'); - parser.pushVariadic(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 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; - } - - 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); - - return { - labels: transformedLabels, - sources: transformedSources - }; -} - -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(','); - } - - const indexOfComma = unwrappedSources.indexOf(','); - if (indexOfComma === -1) { - return [unwrappedSources]; - } - - 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; - } - - const source = unwrappedSources.subarray( - previousComma, - indexOf - ); - sourcesArray.push(source); - previousComma = indexOf + 1; - } - - return sourcesArray; -} From 73d7ae71cc45c5563412ce39c0cac553935a3944 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 7 May 2025 16:10:50 +0300 Subject: [PATCH 109/244] update package-lock.json (#2943) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 0b8ca6ee37e..40d291d9201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8619,7 +8619,7 @@ } }, "packages/redis": { - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "dependencies": { "@redis/bloom": "5.0.1", From 86480aaa74cf557c5ad704ab5e8fb24bdc68c386 Mon Sep 17 00:00:00 2001 From: Clubsandwich Date: Wed, 7 May 2025 22:11:09 +0900 Subject: [PATCH 110/244] fix `cluster.sUnsubscribe` - make `listener` optional (#2946) --- packages/client/lib/cluster/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 12928e71f12..622ac78f467 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -548,7 +548,7 @@ export default class RedisCluster< SUNSUBSCRIBE( channels: string | Array, - listener: PubSubListener, + listener?: PubSubListener, bufferMode?: T ) { return this._self.#slots.executeShardedUnsubscribeCommand( From bb7845dfe39e385d9e02966d54efb211addb79e0 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Thu, 8 May 2025 10:29:05 +0300 Subject: [PATCH 111/244] Disable readOnly for cluster s/pubsub client (#2950) --- packages/client/lib/cluster/cluster-slots.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index c2fde197f4f..0679b200349 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, index >= this.masters.length); + client = this.#createClient(node, false); this.pubSubNode = { address: node.address, @@ -563,7 +563,7 @@ export default class RedisClusterSlots< } async #initiateShardedPubSubClient(master: MasterNode) { - const client = this.#createClient(master, true) + const client = this.#createClient(master, false) .on('server-sunsubscribe', async (channel, listeners) => { try { await this.rediscover(client); From 5e9fea1fd3fec6becd376f09bd6af7069a842ce7 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Thu, 8 May 2025 15:14:30 +0300 Subject: [PATCH 112/244] Update Redis version to 8.0.1-pre (#2952) > FT.AGGREGATE returns an array reply where each row is an array reply and represents a single aggregate result. > The integer reply at position 1 does not represent a valid value. We now calculate the result length bazed on the number of results instead of the integer reply at pos 1 --- .github/workflows/tests.yml | 8 ++++---- packages/search/lib/commands/AGGREGATE.ts | 2 +- packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ad7883a262..395e0251018 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,22 +7,22 @@ on: - v4.0 - v5 paths-ignore: - - '**/*.md' + - "**/*.md" pull_request: branches: - master - v4.0 - v5 paths-ignore: - - '**/*.md' + - "**/*.md" jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - node-version: [ '18', '20', '22' ] - redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-RC2-pre' ] + node-version: ["18", "20", "22"] + redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.1-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index b2589a52a54..9d318743504 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -156,7 +156,7 @@ export default { } return { - total: Number(rawReply[0]), + total: results.length, results }; }, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index dbb834ed7c2..7ddec4b0484 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -73,7 +73,7 @@ describe('PROFILE AGGREGATE', () => { 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.equal(normalizedRes.results.total, 2); assert.ok(Array.isArray(normalizedRes.profile)); assert.equal(normalizedRes.profile[0][0], 'Total profile time'); From ebd03036d69528d11693711b910222a2d1b61e9d Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Fri, 9 May 2025 10:13:32 +0300 Subject: [PATCH 113/244] revert the 'total' count logic in AGGREGATE response introduced in #2952 (#2955) > FT.AGGREGATE returns an array reply where each row is an array reply and represents a single aggregate result. The integer reply at position 1 does not represent a valid value. https://redis.io/docs/latest/commands/ft.aggregate/#return --- packages/search/lib/commands/AGGREGATE.ts | 5 ++++- .../search/lib/commands/PROFILE_AGGREGATE.spec.ts | 14 +++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 9d318743504..0ac3d2e4ce0 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -156,7 +156,10 @@ export default { } return { - total: results.length, + // https://redis.io/docs/latest/commands/ft.aggregate/#return + // FT.AGGREGATE returns an array reply where each row is an array reply and represents a single aggregate result. + // The integer reply at position 1 does not represent a valid value. + total: Number(rawReply[0]), results }; }, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index 7ddec4b0484..82783fbaba9 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -45,7 +45,9 @@ describe('PROFILE AGGREGATE', () => { const res = await client.ft.profileAggregate('index', '*'); const normalizedRes = normalizeObject(res); - assert.equal(normalizedRes.results.total, 2); + // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return + // starts returning valid values + // assert.equal(normalizedRes.results.total, 2); assert.ok(normalizedRes.profile[0] === 'Shards'); assert.ok(Array.isArray(normalizedRes.profile[1])); @@ -73,7 +75,10 @@ describe('PROFILE AGGREGATE', () => { const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); const res = await client.ft.profileAggregate('index', '*'); const normalizedRes = normalizeObject(res); - assert.equal(normalizedRes.results.total, 2); + + // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return + // starts returning valid values + // assert.equal(normalizedRes.results.total, 2); assert.ok(Array.isArray(normalizedRes.profile)); assert.equal(normalizedRes.profile[0][0], 'Total profile time'); @@ -103,8 +108,11 @@ describe('PROFILE AGGREGATE', () => { const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); const res = await client.ft.profileAggregate('index', '*'); + // TODO uncomment after https://redis.io/docs/latest/commands/ft.aggregate/#return + // starts returning valid values + // assert.equal(res.Results.total_results, 2); + const normalizedRes = normalizeObject(res); - assert.equal(normalizedRes.Results.total_results, 2); assert.ok(normalizedRes.Profile.Shards); }, GLOBAL.SERVERS.OPEN_3); }); From 6f961bd7154c899907cc6419af9ca362c58ec81c Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 14 May 2025 17:23:22 +0300 Subject: [PATCH 114/244] fix(client): cache subsequent clients (#2963) * fix(client): cache subsequent clients we dont need to recreate a client if its config hasnt changed fixes #2954 * handle circular structures * make cache generic --- packages/client/lib/client/index.ts | 30 ++++--- packages/client/lib/client/pool.ts | 27 +++--- packages/client/lib/cluster/index.ts | 30 ++++--- .../client/lib/single-entry-cache.spec.ts | 85 +++++++++++++++++++ packages/client/lib/single-entry-cache.ts | 37 ++++++++ 5 files changed, 178 insertions(+), 31 deletions(-) create mode 100644 packages/client/lib/single-entry-cache.spec.ts create mode 100644 packages/client/lib/single-entry-cache.ts diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index f9e95025c6a..f3e72a3a172 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -17,6 +17,7 @@ import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; import { RedisPoolOptions, RedisClientPool } from './pool'; import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../commands/generic-transformers'; import { BasicCommandParser, CommandParser } from './parser'; +import SingleEntryCache from '../single-entry-cache'; export interface RedisClientOptions< M extends RedisModules = RedisModules, @@ -206,23 +207,32 @@ export default class RedisClient< } } + static #SingleEntryCache = new SingleEntryCache() + 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); + + let Client = RedisClient.#SingleEntryCache.get(config); + if (!Client) { + 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); + + RedisClient.#SingleEntryCache.set(config, Client); + } return ( options?: Omit, keyof Exclude> diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index a08377e3d38..ec89f0c39e3 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -8,6 +8,7 @@ import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumen import { CommandOptions } from './commands-queue'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import { BasicCommandParser } from './parser'; +import SingleEntryCache from '../single-entry-cache'; export interface RedisPoolOptions { /** @@ -110,6 +111,8 @@ export class RedisClientPool< }; } + static #SingleEntryCache = new SingleEntryCache(); + static create< M extends RedisModules, F extends RedisFunctions, @@ -120,17 +123,21 @@ export class RedisClientPool< 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); + let Pool = RedisClientPool.#SingleEntryCache.get(clientOptions); + if(!Pool) { + 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); + RedisClientPool.#SingleEntryCache.set(clientOptions, Pool); + } // returning a "proxy" to prevent the namespaces._self to leak between "proxies" return Object.create( diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 622ac78f467..8b37f9c1aa7 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -12,6 +12,7 @@ import { RedisTcpSocketOptions } from '../client/socket'; import ASKING from '../commands/ASKING'; import { BasicCommandParser } from '../client/parser'; import { parseArgs } from '../commands/generic-transformers'; +import SingleEntryCache from '../single-entry-cache'; interface ClusterCommander< M extends RedisModules, @@ -213,6 +214,8 @@ export default class RedisCluster< }; } + static #SingleEntryCache = new SingleEntryCache(); + static factory< M extends RedisModules = {}, F extends RedisFunctions = {}, @@ -221,17 +224,22 @@ export default class RedisCluster< 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); + + let Cluster = RedisCluster.#SingleEntryCache.get(config); + if (!Cluster) { + 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); + RedisCluster.#SingleEntryCache.set(config, Cluster); + } return (options?: Omit>) => { // returning a "proxy" to prevent the namespaces._self to leak between "proxies" diff --git a/packages/client/lib/single-entry-cache.spec.ts b/packages/client/lib/single-entry-cache.spec.ts new file mode 100644 index 00000000000..ef535738eac --- /dev/null +++ b/packages/client/lib/single-entry-cache.spec.ts @@ -0,0 +1,85 @@ +import assert from 'node:assert'; +import SingleEntryCache from './single-entry-cache'; + +describe('SingleEntryCache', () => { + let cache: SingleEntryCache; + beforeEach(() => { + cache = new SingleEntryCache(); + }); + + it('should return undefined when getting from empty cache', () => { + assert.strictEqual(cache.get({ key: 'value' }), undefined); + }); + + it('should return the cached instance when getting with the same key object', () => { + const keyObj = { key: 'value' }; + const instance = { data: 'test data' }; + + cache.set(keyObj, instance); + assert.strictEqual(cache.get(keyObj), instance); + }); + + it('should return undefined when getting with a different key object', () => { + const keyObj1 = { key: 'value1' }; + const keyObj2 = { key: 'value2' }; + const instance = { data: 'test data' }; + + cache.set(keyObj1, instance); + assert.strictEqual(cache.get(keyObj2), undefined); + }); + + it('should update the cached instance when setting with the same key object', () => { + const keyObj = { key: 'value' }; + const instance1 = { data: 'test data 1' }; + const instance2 = { data: 'test data 2' }; + + cache.set(keyObj, instance1); + assert.strictEqual(cache.get(keyObj), instance1); + + cache.set(keyObj, instance2); + assert.strictEqual(cache.get(keyObj), instance2); + }); + + it('should handle undefined key object', () => { + const instance = { data: 'test data' }; + + cache.set(undefined, instance); + assert.strictEqual(cache.get(undefined), instance); + }); + + it('should handle complex objects as keys', () => { + const keyObj = { + id: 123, + nested: { + prop: 'value', + array: [1, 2, 3] + } + }; + const instance = { data: 'complex test data' }; + + cache.set(keyObj, instance); + assert.strictEqual(cache.get(keyObj), instance); + }); + + it('should consider objects with same properties but different order as different keys', () => { + const keyObj1 = { a: 1, b: 2 }; + const keyObj2 = { b: 2, a: 1 }; // Same properties but different order + const instance = { data: 'test data' }; + + cache.set(keyObj1, instance); + + assert.strictEqual(cache.get(keyObj2), undefined); + }); + + it('should handle circular structures', () => { + const keyObj: any = {}; + keyObj.self = keyObj; + + const instance = { data: 'test data' }; + + cache.set(keyObj, instance); + + assert.strictEqual(cache.get(keyObj), instance); + }); + +}); diff --git a/packages/client/lib/single-entry-cache.ts b/packages/client/lib/single-entry-cache.ts new file mode 100644 index 00000000000..5c65df96660 --- /dev/null +++ b/packages/client/lib/single-entry-cache.ts @@ -0,0 +1,37 @@ +export default class SingleEntryCache { + #cached?: V; + #serializedKey?: string; + + /** + * Retrieves an instance from the cache based on the provided key object. + * + * @param keyObj - The key object to look up in the cache. + * @returns The cached instance if found, undefined otherwise. + * + * @remarks + * This method uses JSON.stringify for comparison, which may not work correctly + * if the properties in the key object are rearranged or reordered. + */ + get(keyObj?: K): V | undefined { + return JSON.stringify(keyObj, makeCircularReplacer()) === this.#serializedKey ? this.#cached : undefined; + } + + set(keyObj: K | undefined, obj: V) { + this.#cached = obj; + this.#serializedKey = JSON.stringify(keyObj, makeCircularReplacer()); + } +} + +function makeCircularReplacer() { + const seen = new WeakSet(); + return function serialize(_: string, value: any) { + if (value && typeof value === 'object') { + if (seen.has(value)) { + return 'circular'; + } + seen.add(value); + return value; + } + return value; + } +} \ No newline at end of file From f01f1014cb472e88a5e19f3386341023a8e7a7c8 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Mon, 19 May 2025 15:11:47 +0300 Subject: [PATCH 115/244] Client Side Caching (#2947) * CSC POC ontop of Parser * add csc file that weren't merged after patch * address review comments * nits to try and fix github * last change from review * Update client-side cache and improve documentation * Add client side caching RESP3 validation * Add documentation for RESP and unstableResp3 options * Add comprehensive cache statistics The `CacheStats` class provides detailed metrics like hit/miss counts, load success/failure counts, total load time, and eviction counts. It also offers derived metrics such as hit/miss rates, load failure rate, and average load penalty. The design is inspired by Caffeine. `BasicClientSideCache` now uses a `StatsCounter` to accumulate these statistics, exposed via a new `stats()` method. The previous `cacheHits()` and `cacheMisses()` methods have been removed. A `recordStats` option (default: true) in `ClientSideCacheConfig` allows disabling statistics collection. --------- Co-authored-by: Shaya Potter --- README.md | 18 + docs/v5.md | 97 ++ packages/client/index.ts | 3 + packages/client/lib/RESP/types.ts | 10 +- packages/client/lib/client/cache.spec.ts | 700 ++++++++++++++ packages/client/lib/client/cache.ts | 870 ++++++++++++++++++ packages/client/lib/client/commands-queue.ts | 23 +- packages/client/lib/client/index.spec.ts | 64 +- packages/client/lib/client/index.ts | 170 +++- packages/client/lib/client/linked-list.ts | 10 +- packages/client/lib/client/parser.ts | 11 + packages/client/lib/client/pool.ts | 93 +- packages/client/lib/client/socket.ts | 7 + .../client/lib/cluster/cluster-slots.spec.ts | 48 + packages/client/lib/cluster/cluster-slots.ts | 25 +- packages/client/lib/cluster/index.ts | 87 +- packages/client/lib/commands/GEOSEARCH.ts | 5 - .../client/lib/commands/GEOSEARCHSTORE.ts | 7 +- packages/client/lib/sentinel/index.spec.ts | 76 ++ packages/client/lib/sentinel/index.ts | 36 +- packages/client/lib/sentinel/test-util.ts | 14 +- packages/client/lib/sentinel/types.ts | 36 + packages/client/lib/sentinel/utils.ts | 2 +- packages/redis/README.md | 17 + packages/test-utils/lib/index.ts | 2 +- 25 files changed, 2330 insertions(+), 101 deletions(-) create mode 100644 packages/client/lib/client/cache.spec.ts create mode 100644 packages/client/lib/client/cache.ts create mode 100644 packages/client/lib/cluster/cluster-slots.spec.ts diff --git a/README.md b/README.md index ac6394461f1..38615ee519e 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,24 @@ of sending a `QUIT` command to the server, the client can simply close the netwo client.destroy(); ``` +### Client Side Caching + +Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid. + +```typescript +// Enable client side caching with RESP3 +const client = createClient({ + RESP: 3, + clientSideCache: { + ttl: 0, // Time-to-live (0 = no expiration) + maxEntries: 0, // Maximum entries (0 = unlimited) + evictPolicy: "LRU" // Eviction policy: "LRU" or "FIFO" + } +}); +``` + +See the [V5 documentation](./docs/v5.md#client-side-caching) for more details and advanced usage. + ### Auto-Pipelining Node Redis will automatically pipeline requests that are made during the same "tick". diff --git a/docs/v5.md b/docs/v5.md index a3d0ab68389..1784ae5bd74 100644 --- a/docs/v5.md +++ b/docs/v5.md @@ -89,3 +89,100 @@ await multi.exec(); // Array await multi.exec<'typed'>(); // [string] await multi.execTyped(); // [string] ``` + +# Client Side Caching + +Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The server will notify the client when cached results are no longer valid. + +Client Side Caching is only supported with RESP3. + +## Usage + +There are two ways to implement client side caching: + +### Anonymous Cache + +```javascript +const client = createClient({ + RESP: 3, + clientSideCache: { + ttl: 0, // Time-to-live in milliseconds (0 = no expiration) + maxEntries: 0, // Maximum entries to store (0 = unlimited) + evictPolicy: "LRU" // Eviction policy: "LRU" or "FIFO" + } +}); +``` + +In this instance, the cache is managed internally by the client. + +### Controllable Cache + +```javascript +import { BasicClientSideCache } from 'redis'; + +const cache = new BasicClientSideCache({ + ttl: 0, + maxEntries: 0, + evictPolicy: "LRU" +}); + +const client = createClient({ + RESP: 3, + clientSideCache: cache +}); +``` + +With this approach, you have direct access to the cache object for more control: + +```javascript +// Manually invalidate keys +cache.invalidate(key); + +// Clear the entire cache +cache.clear(); + +// Get cache metrics +// `cache.stats()` returns a `CacheStats` object with comprehensive statistics. +const statistics = cache.stats(); + +// Key metrics: +const hits = statistics.hitCount; // Number of cache hits +const misses = statistics.missCount; // Number of cache misses +const hitRate = statistics.hitRate(); // Cache hit rate (0.0 to 1.0) + +// Many other metrics are available on the `statistics` object, e.g.: +// statistics.missRate(), statistics.loadSuccessCount, +// statistics.averageLoadPenalty(), statistics.requestCount() +``` + +## Pooled Caching + +Client side caching also works with client pools. For pooled clients, the cache is shared across all clients in the pool: + +```javascript +const client = createClientPool({RESP: 3}, { + clientSideCache: { + ttl: 0, + maxEntries: 0, + evictPolicy: "LRU" + }, + minimum: 5 +}); +``` + +For a controllable pooled cache: + +```javascript +import { BasicPooledClientSideCache } from 'redis'; + +const cache = new BasicPooledClientSideCache({ + ttl: 0, + maxEntries: 0, + evictPolicy: "LRU" +}); + +const client = createClientPool({RESP: 3}, { + clientSideCache: cache, + minimum: 5 +}); +``` diff --git a/packages/client/index.ts b/packages/client/index.ts index 1f05bc30341..2deb0c39b47 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -34,3 +34,6 @@ export { GEO_REPLY_WITH, GeoReplyWith } from './lib/commands/GEOSEARCH_WITH'; export { SetOptions } from './lib/commands/SET'; export { REDIS_FLUSH_MODES } from './lib/commands/FLUSHALL'; + +export { BasicClientSideCache, BasicPooledClientSideCache } from './lib/client/cache'; + diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts index 692c433a49d..8749bbdc7b0 100644 --- a/packages/client/lib/RESP/types.ts +++ b/packages/client/lib/RESP/types.ts @@ -314,11 +314,17 @@ export interface CommanderConfig< functions?: F; scripts?: S; /** - * TODO + * Specifies the Redis Serialization Protocol version to use. + * RESP2 is the default (value 2), while RESP3 (value 3) provides + * additional data types and features introduced in Redis 6.0. */ RESP?: RESP; /** - * TODO + * When set to true, enables commands that have unstable RESP3 implementations. + * When using RESP3 protocol, commands marked as having unstable RESP3 support + * will throw an error unless this flag is explicitly set to true. + * This primarily affects modules like Redis Search where response formats + * in RESP3 mode may change in future versions. */ unstableResp3?: boolean; } diff --git a/packages/client/lib/client/cache.spec.ts b/packages/client/lib/client/cache.spec.ts new file mode 100644 index 00000000000..55f2672c26c --- /dev/null +++ b/packages/client/lib/client/cache.spec.ts @@ -0,0 +1,700 @@ +import assert from "assert"; +import testUtils, { GLOBAL } from "../test-utils" +import { BasicClientSideCache, BasicPooledClientSideCache, CacheStats } from "./cache" +import { REDIS_FLUSH_MODES } from "../commands/FLUSHALL"; +import { once } from 'events'; + +describe("Client Side Cache", () => { + describe('Basic Cache', () => { + const csc = new BasicClientSideCache({ maxEntries: 10 }); + + testUtils.testWithClient('Basic Cache Miss', async client => { + csc.clear(); + + await client.set("x", 1); + await client.get("x"); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('Basic Cache Hit', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + assert.equal(await client.get("x"), '1'); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 1, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('Max Cache Entries', async client => { + csc.clear(); + + await client.set('1', 1); + assert.equal(await client.get('1'), '1'); + assert.equal(await client.get('2'), null); + assert.equal(await client.get('3'), null); + assert.equal(await client.get('4'), null); + assert.equal(await client.get('5'), null); + assert.equal(await client.get('6'), null); + assert.equal(await client.get('7'), null); + assert.equal(await client.get('8'), null); + assert.equal(await client.get('9'), null); + assert.equal(await client.get('10'), null); + assert.equal(await client.get('11'), null); + assert.equal(await client.get('1'), '1'); + + assert.equal(csc.stats().missCount, 12, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('LRU works correctly', async client => { + csc.clear(); + + await client.set('1', 1); + assert.equal(await client.get('1'), '1'); + assert.equal(await client.get('2'), null); + assert.equal(await client.get('3'), null); + assert.equal(await client.get('4'), null); + assert.equal(await client.get('5'), null); + assert.equal(await client.get('1'), '1'); + assert.equal(await client.get('6'), null); + assert.equal(await client.get('7'), null); + assert.equal(await client.get('8'), null); + assert.equal(await client.get('9'), null); + assert.equal(await client.get('10'), null); + assert.equal(await client.get('11'), null); + assert.equal(await client.get('1'), '1'); + + assert.equal(csc.stats().missCount, 11, "Cache Misses"); + assert.equal(csc.stats().hitCount, 2, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('Basic Cache Clear', async client => { + csc.clear(); + + await client.set("x", 1); + await client.get("x"); + csc.clear(); + await client.get("x"); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('Null Invalidate acts as clear', async client => { + csc.clear(); + + await client.set("x", 1); + await client.get("x"); + csc.invalidate(null); + await client.get("x"); + + assert.equal(2, csc.stats().missCount, "Cache Misses"); + assert.equal(0, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('flushdb causes an invalidate null', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + await client.flushDb(REDIS_FLUSH_MODES.SYNC); + assert.equal(await client.get("x"), null); + + assert.equal(csc.stats().missCount, 2, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient('Basic Cache Invalidate', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1', 'first get'); + await client.set("x", 2); + assert.equal(await client.get("x"), '2', 'second get'); + await client.set("x", 3); + assert.equal(await client.get("x"), '3', 'third get'); + + assert.equal(csc.stats().missCount, 3, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient("Cached Replies Don't Mutate", async client => { + csc.clear(); + + await client.set("x", 1); + await client.set('y', 2); + const ret1 = await client.mGet(['x', 'y']); + assert.deepEqual(ret1, ['1', '2'], 'first mGet'); + ret1[0] = '4'; + const ret2 = await client.mGet(['x', 'y']); + assert.deepEqual(ret2, ['1', '2'], 'second mGet'); + ret2[0] = '8'; + const ret3 = await client.mGet(['x', 'y']); + assert.deepEqual(ret3, ['1', '2'], 'third mGet'); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 2, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClient("Cached cleared on disconnect", async client => { + csc.clear(); + + await client.set("x", 1); + await client.set('y', 2); + const ret1 = await client.mGet(['x', 'y']); + assert.deepEqual(ret1, ['1', '2'], 'first mGet'); + + assert.equal(csc.stats().missCount, 1, "first Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "first Cache Hits"); + + await client.close(); + + await client.connect(); + + const ret2 = await client.mGet(['x', 'y']); + assert.deepEqual(ret2, ['1', '2'], 'second mGet'); + + assert.equal(csc.stats().missCount, 1, "second Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "second Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + }); + + describe("Pooled Cache", () => { + const csc = new BasicPooledClientSideCache(); + + testUtils.testWithClient('Virtual Pool Disconnect', async client1 => { + const client2 = client1.duplicate(); + await client2.connect() + + assert.equal(await client2.get("x"), null); + assert.equal(await client1.get("x"), null); + + assert.equal(1, csc.stats().missCount, "Cache Misses"); + assert.equal(1, csc.stats().hitCount, "Cache Hits"); + + await client2.close(); + + assert.equal(await client1.get("x"), null); + assert.equal(await client1.get("x"), null); + + assert.equal(2, csc.stats().missCount, "Cache Misses"); + assert.equal(2, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + + testUtils.testWithClientPool('Basic Cache Miss and Clear', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + + assert.equal(1, csc.stats().missCount, "Cache Misses"); + assert.equal(0, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + poolOptions: { + minimum: 5, + maximum: 5, + acquireTimeout: 0, + cleanupDelay: 1, + clientSideCache: csc + } + }) + + testUtils.testWithClientPool('Basic Cache Hit', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + assert.equal(await client.get("x"), '1'); + assert.equal(await client.get("x"), '1'); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 2, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + poolOptions: { + minimum: 5, + maximum: 5, + acquireTimeout: 0, + cleanupDelay: 1, + clientSideCache: csc + } + }) + + testUtils.testWithClientPool('Basic Cache Manually Invalidate', async client => { + csc.clear(); + + await client.set("x", 1); + + assert.equal(await client.get("x"), '1', 'first get'); + + let p: Promise> = once(csc, 'invalidate'); + await client.set("x", 2); + let [i] = await p; + + assert.equal(await client.get("x"), '2', 'second get'); + + p = once(csc, 'invalidate'); + await client.set("x", 3); + [i] = await p; + + assert.equal(await client.get("x"), '3'); + + assert.equal(csc.stats().missCount, 3, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + poolOptions: { + minimum: 5, + maximum: 5, + acquireTimeout: 0, + cleanupDelay: 1, + clientSideCache: csc + } + }) + + testUtils.testWithClientPool('Basic Cache Invalidate via message', async client => { + csc.clear(); + + await client.set('x', 1); + await client.set('y', 2); + + assert.deepEqual(await client.mGet(['x', 'y']), ['1', '2'], 'first mGet'); + + assert.equal(csc.stats().missCount, 1, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + + let p: Promise> = once(csc, 'invalidate'); + await client.set("x", 3); + let [i] = await p; + + assert.equal(i, 'x'); + + assert.deepEqual(await client.mGet(['x', 'y']), ['3', '2'], 'second mGet'); + + assert.equal(csc.stats().missCount, 2, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + + p = once(csc, 'invalidate'); + await client.set("y", 4); + [i] = await p; + + assert.equal(i, 'y'); + + assert.deepEqual(await client.mGet(['x', 'y']), ['3', '4'], 'second mGet'); + + assert.equal(csc.stats().missCount, 3, "Cache Misses"); + assert.equal(csc.stats().hitCount, 0, "Cache Hits"); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + poolOptions: { + minimum: 5, + maximum: 5, + acquireTimeout: 0, + cleanupDelay: 1, + clientSideCache: csc + } + }) + }); + + describe('Cluster Caching', () => { + const csc = new BasicPooledClientSideCache(); + + testUtils.testWithCluster('Basic Cache Miss and Clear', async client => { + csc.clear(); + + await client.set("x", 1); + await client.get("x"); + await client.set("y", 1); + await client.get("y"); + + assert.equal(2, csc.stats().missCount, "Cache Misses"); + assert.equal(0, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + RESP: 3, + clientSideCache: csc + } + }) + + testUtils.testWithCluster('Basic Cache Hit', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + assert.equal(await client.get("x"), '1'); + assert.equal(await client.get("x"), '1'); + await client.set("y", 1); + assert.equal(await client.get("y"), '1'); + assert.equal(await client.get("y"), '1'); + assert.equal(await client.get("y"), '1'); + + assert.equal(2, csc.stats().missCount, "Cache Misses"); + assert.equal(4, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + RESP: 3, + clientSideCache: csc + } + }) + + testUtils.testWithCluster('Basic Cache Invalidate', async client => { + csc.clear(); + + await client.set("x", 1); + assert.equal(await client.get("x"), '1'); + await client.set("x", 2); + assert.equal(await client.get("x"), '2'); + await client.set("x", 3); + assert.equal(await client.get("x"), '3'); + + await client.set("y", 1); + assert.equal(await client.get("y"), '1'); + await client.set("y", 2); + assert.equal(await client.get("y"), '2'); + await client.set("y", 3); + assert.equal(await client.get("y"), '3'); + + assert.equal(6, csc.stats().missCount, "Cache Misses"); + assert.equal(0, csc.stats().hitCount, "Cache Hits"); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + RESP: 3, + clientSideCache: csc + } + }) + }); + describe("CacheStats", () => { + describe("CacheStats.of()", () => { + it("should correctly initialize stats and calculate derived values", () => { + const stats = CacheStats.of(10, 5, 8, 2, 100, 3); + assert.strictEqual(stats.hitCount, 10, "hitCount should be 10"); + assert.strictEqual(stats.missCount, 5, "missCount should be 5"); + assert.strictEqual(stats.loadSuccessCount, 8, "loadSuccessCount should be 8"); + assert.strictEqual(stats.loadFailureCount, 2, "loadFailureCount should be 2"); + assert.strictEqual(stats.totalLoadTime, 100, "totalLoadTime should be 100"); + assert.strictEqual(stats.evictionCount, 3, "evictionCount should be 3"); + + assert.strictEqual(stats.requestCount(), 15, "requestCount should be 15 (10 hits + 5 misses)"); + assert.strictEqual(stats.hitRate(), 10 / 15, "hitRate should be 10/15"); + assert.strictEqual(stats.missRate(), 5 / 15, "missRate should be 5/15"); + assert.strictEqual(stats.loadCount(), 10, "loadCount should be 10 (8 success + 2 failure)"); + assert.strictEqual(stats.loadFailureRate(), 2 / 10, "loadFailureRate should be 2/10"); + assert.strictEqual(stats.averageLoadPenalty(), 100 / 10, "averageLoadPenalty should be 10 (100 time / 10 loads)"); + }); + + it("should handle zero values and division by zero for derived values", () => { + const stats = CacheStats.of(0, 0, 0, 0, 0, 0); + assert.strictEqual(stats.hitCount, 0, "hitCount"); + assert.strictEqual(stats.missCount, 0, "missCount"); + assert.strictEqual(stats.loadSuccessCount, 0, "loadSuccessCount"); + assert.strictEqual(stats.loadFailureCount, 0, "loadFailureCount"); + assert.strictEqual(stats.totalLoadTime, 0, "totalLoadTime"); + assert.strictEqual(stats.evictionCount, 0, "evictionCount"); + + assert.strictEqual(stats.requestCount(), 0, "requestCount should be 0"); + assert.strictEqual(stats.hitRate(), 1, "hitRate should be 1 for 0 requests"); + assert.strictEqual(stats.missRate(), 0, "missRate should be 0 for 0 requests"); + assert.strictEqual(stats.loadCount(), 0, "loadCount should be 0"); + assert.strictEqual(stats.loadFailureRate(), 0, "loadFailureRate should be 0 for 0 loads"); + assert.strictEqual(stats.averageLoadPenalty(), 0, "averageLoadPenalty should be 0 for 0 loads"); + }); + }); + + describe("CacheStats.empty()", () => { + it("should return stats with all zero counts and 0 for rates/penalties", () => { + const stats = CacheStats.empty(); + assert.strictEqual(stats.hitCount, 0, "empty.hitCount"); + assert.strictEqual(stats.missCount, 0, "empty.missCount"); + assert.strictEqual(stats.loadSuccessCount, 0, "empty.loadSuccessCount"); + assert.strictEqual(stats.loadFailureCount, 0, "empty.loadFailureCount"); + assert.strictEqual(stats.totalLoadTime, 0, "empty.totalLoadTime"); + assert.strictEqual(stats.evictionCount, 0, "empty.evictionCount"); + + assert.strictEqual(stats.requestCount(), 0, "empty.requestCount"); + assert.strictEqual(stats.hitRate(), 1, "empty.hitRate should be 1"); + assert.strictEqual(stats.missRate(), 0, "empty.missRate should be 0"); + assert.strictEqual(stats.loadCount(), 0, "empty.loadCount"); + assert.strictEqual(stats.loadFailureRate(), 0, "empty.loadFailureRate should be 0"); + assert.strictEqual(stats.averageLoadPenalty(), 0, "empty.averageLoadPenalty should be 0"); + }); + }); + + describe("instance methods", () => { + const stats1 = CacheStats.of(10, 5, 8, 2, 100, 3); + const stats2 = CacheStats.of(20, 10, 12, 3, 200, 5); + + describe("plus()", () => { + it("should correctly add two CacheStats instances", () => { + const sum = stats1.plus(stats2); + assert.strictEqual(sum.hitCount, 30); + assert.strictEqual(sum.missCount, 15); + assert.strictEqual(sum.loadSuccessCount, 20); + assert.strictEqual(sum.loadFailureCount, 5); + assert.strictEqual(sum.totalLoadTime, 300); + assert.strictEqual(sum.evictionCount, 8); + }); + + it("should correctly sum large numbers", () => { + const statsC = CacheStats.of(Number.MAX_VALUE, 1, 1, 1, 1, 1); + const statsD = CacheStats.of(Number.MAX_VALUE, 1, 1, 1, 1, 1); + const sum = statsC.plus(statsD); + assert.strictEqual(sum.hitCount, Infinity, "Summing MAX_VALUE should result in Infinity"); + }); + }); + + describe("minus()", () => { + it("should correctly subtract one CacheStats instance from another, flooring at 0", () => { + const diff = stats2.minus(stats1); + assert.strictEqual(diff.hitCount, 10); + assert.strictEqual(diff.missCount, 5); + assert.strictEqual(diff.loadSuccessCount, 4); + assert.strictEqual(diff.loadFailureCount, 1); + assert.strictEqual(diff.totalLoadTime, 100); + assert.strictEqual(diff.evictionCount, 2); + }); + + it("should floor results at 0 if minuend is smaller than subtrahend", () => { + const sSmall = CacheStats.of(5, 2, 1, 0, 10, 1); + const sLarge = CacheStats.of(10, 5, 2, 1, 20, 2); + const diff = sSmall.minus(sLarge); + assert.strictEqual(diff.hitCount, 0, "hitCount should be floored at 0 (5 - 10)"); + assert.strictEqual(diff.missCount, 0, "missCount should be floored at 0 (2 - 5)"); + assert.strictEqual(diff.loadSuccessCount, 0, "loadSuccessCount should be floored at 0 (1 - 2)"); + assert.strictEqual(diff.loadFailureCount, 0, "loadFailureCount should be floored at 0 (0 - 1)"); + assert.strictEqual(diff.totalLoadTime, 0, "totalLoadTime should be floored at 0 (10 - 20)"); + assert.strictEqual(diff.evictionCount, 0, "evictionCount should be floored at 0 (1 - 2)"); + }); + }); + + describe("hitRate()", () => { + it("should return 0 if requestCount is 0", () => { + const stats = CacheStats.of(0, 0, 0, 0, 0, 0); + assert.strictEqual(stats.hitRate(), 1); + }); + it("should return 0 if hitCount is 0 but missCount > 0", () => { + const stats = CacheStats.of(0, 1, 0, 0, 0, 0); + assert.strictEqual(stats.hitRate(), 0); + }); + it("should return 1 if missCount is 0 but hitCount > 0", () => { + const stats = CacheStats.of(1, 0, 0, 0, 0, 0); + assert.strictEqual(stats.hitRate(), 1); + }); + }); + + describe("missRate()", () => { + it("should return 0 if requestCount is 0", () => { + const stats = CacheStats.of(0, 0, 0, 0, 0, 0); + assert.strictEqual(stats.missRate(), 0); + }); + it("should return 1 if hitCount is 0 but missCount > 0", () => { + const stats = CacheStats.of(0, 1, 0, 0, 0, 0); + assert.strictEqual(stats.missRate(), 1); + }); + it("should return 0 if missCount is 0 but hitCount > 0", () => { + const stats = CacheStats.of(1, 0, 0, 0, 0, 0); + assert.strictEqual(stats.missRate(), 0); + }); + }); + + describe("loadFailureRate()", () => { + it("should return 0 if loadCount is 0", () => { + const stats = CacheStats.of(0, 0, 0, 0, 0, 0); + assert.strictEqual(stats.loadFailureRate(), 0); + }); + it("should return 0 if loadFailureCount is 0 but loadSuccessCount > 0", () => { + const stats = CacheStats.of(0, 0, 1, 0, 10, 0); + assert.strictEqual(stats.loadFailureRate(), 0); + }); + it("should return 1 if loadSuccessCount is 0 but loadFailureCount > 0", () => { + const stats = CacheStats.of(0, 0, 0, 1, 10, 0); + assert.strictEqual(stats.loadFailureRate(), 1); + }); + }); + + describe("averageLoadPenalty()", () => { + it("should return 0 if loadCount is 0, even if totalLoadTime > 0", () => { + const stats = CacheStats.of(0, 0, 0, 0, 100, 0); + assert.strictEqual(stats.averageLoadPenalty(), 0); + }); + it("should return 0 if totalLoadTime is 0 and loadCount > 0", () => { + const stats = CacheStats.of(0, 0, 1, 1, 0, 0); + assert.strictEqual(stats.averageLoadPenalty(), 0); + }); + }); + }); + }); + it('should reflect comprehensive cache operations in stats via BasicClientSideCache', async function () { + + const csc = new BasicClientSideCache({ + maxEntries: 2, // Small size to easily trigger evictions + }); + + testUtils.testWithClient('comprehensive_stats_run', async client => { + + // --- Phase 1: Initial misses and loads --- + await client.set('keyA', 'valueA_1'); + assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA first time"); + assert.strictEqual(csc.stats().missCount, 1); + assert.strictEqual(csc.stats().loadSuccessCount, 1); + + await client.set('keyB', 'valueB_1'); + assert.strictEqual(await client.get('keyB'), 'valueB_1', "Get keyB first time"); + assert.strictEqual(csc.stats().missCount, 2); + assert.strictEqual(csc.stats().loadSuccessCount, 2); + + // --- Phase 2: Cache hits --- + assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA second time (hit)"); + assert.strictEqual(csc.stats().hitCount, 1); + + assert.strictEqual(await client.get('keyB'), 'valueB_1', "Get keyB second time (hit)"); + assert.strictEqual(csc.stats().hitCount, 2); + + + // --- Phase 3: Trigger evictions and more misses/loads --- + await client.set('keyC', 'valueC_1'); + assert.strictEqual(await client.get('keyC'), 'valueC_1', "Get keyC first time (evicts keyA)"); + assert.strictEqual(csc.stats().missCount, 3); + assert.strictEqual(csc.stats().loadSuccessCount, 3); + assert.strictEqual(csc.stats().evictionCount, 1); + + + assert.strictEqual(await client.get('keyA'), 'valueA_1', "Get keyA again (miss after eviction)"); + assert.strictEqual(csc.stats().missCount, 4); + assert.strictEqual(csc.stats().loadSuccessCount, 4); + assert.strictEqual(csc.stats().evictionCount, 2); + + + // --- Phase 4: More hits --- + assert.strictEqual(await client.get('keyC'), 'valueC_1', "Get keyC again (hit)"); + assert.strictEqual(csc.stats().hitCount, 3); + + // --- Phase 5: Update a key (results in invalidation, then miss/load on next GET) --- + // Note: A SET operation on an existing cached key should invalidate it. + // The invalidation itself isn't directly a "hit" or "miss" for stats, + // but the *next* GET will be a miss. + await client.set('keyA', 'valueA_2'); + assert.strictEqual(await client.get('keyA'), 'valueA_2', "Get keyA after SET (miss due to invalidation)"); + + assert.strictEqual(csc.stats().hitCount, 3); + assert.strictEqual(csc.stats().loadSuccessCount, 5); + + + + const stats = csc.stats() + + assert.strictEqual(stats.hitCount, 3, "Final hitCount"); + assert.strictEqual(stats.missCount, 5, "Final missCount"); + assert.strictEqual(stats.loadSuccessCount, 5, "Final loadSuccessCount"); + assert.strictEqual(stats.loadFailureCount, 0, "Final loadFailureCount (expected 0 for this test)"); + assert.strictEqual(stats.evictionCount, 2, "Final evictionCount"); + assert.ok(stats.totalLoadTime >= 0, "Final totalLoadTime should be non-negative"); + + assert.strictEqual(stats.requestCount(), 8, "Final requestCount (5 misses + 3 hits)"); + assert.strictEqual(stats.hitRate(), 3 / 8, "Final hitRate"); + assert.strictEqual(stats.missRate(), 5 / 8, "Final missRate"); + + assert.strictEqual(stats.loadCount(), 5, "Final loadCount (5 success + 0 failure)"); + assert.strictEqual(stats.loadFailureRate(), 0, "Final loadFailureRate (0 failures / 5 loads)"); + + if (stats.loadCount() > 0) { + assert.ok(stats.averageLoadPenalty() >= 0, "Final averageLoadPenalty should be non-negative"); + assert.strictEqual(stats.averageLoadPenalty(), stats.totalLoadTime / stats.loadCount(), "Average load penalty calculation"); + } else { + assert.strictEqual(stats.averageLoadPenalty(), 0, "Final averageLoadPenalty should be 0 if no loads"); + } + + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientSideCache: csc + } + }); + }); +}); diff --git a/packages/client/lib/client/cache.ts b/packages/client/lib/client/cache.ts new file mode 100644 index 00000000000..7254352ee8f --- /dev/null +++ b/packages/client/lib/client/cache.ts @@ -0,0 +1,870 @@ +import { EventEmitter } from 'stream'; +import RedisClient from '.'; +import { RedisArgument, ReplyUnion, TransformReply, TypeMapping } from '../RESP/types'; +import { BasicCommandParser } from './parser'; + +/** + * A snapshot of cache statistics. + * + * This class provides an immutable view of the cache's operational statistics at a particular + * point in time. It is heavily inspired by the statistics reporting capabilities found in + * Ben Manes's Caffeine cache (https://github.com/ben-manes/caffeine). + * + * Instances of `CacheStats` are typically obtained from a {@link StatsCounter} and can be used + * for performance monitoring, debugging, or logging. It includes metrics such as hit rate, + * miss rate, load success/failure rates, average load penalty, and eviction counts. + * + * All statistics are non-negative. Rates and averages are typically in the range `[0.0, 1.0]`, + * or `0` if the an operation has not occurred (e.g. hit rate is 0 if there are no requests). + * + * Cache statistics are incremented according to specific rules: + * - When a cache lookup encounters an existing entry, hitCount is incremented. + * - When a cache lookup encounters a missing entry, missCount is incremented. + * - When a new entry is successfully loaded, loadSuccessCount is incremented and the + * loading time is added to totalLoadTime. + * - When an entry fails to load, loadFailureCount is incremented and the + * loading time is added to totalLoadTime. + * - When an entry is evicted due to size constraints or expiration, + * evictionCount is incremented. + */ +export class CacheStats { + /** + * Creates a new CacheStats instance with the specified statistics. + */ + private constructor( + public readonly hitCount: number, + public readonly missCount: number, + public readonly loadSuccessCount: number, + public readonly loadFailureCount: number, + public readonly totalLoadTime: number, + public readonly evictionCount: number + ) { + if ( + hitCount < 0 || + missCount < 0 || + loadSuccessCount < 0 || + loadFailureCount < 0 || + totalLoadTime < 0 || + evictionCount < 0 + ) { + throw new Error('All statistics values must be non-negative'); + } + } + + /** + * Creates a new CacheStats instance with the specified statistics. + * + * @param hitCount - Number of cache hits + * @param missCount - Number of cache misses + * @param loadSuccessCount - Number of successful cache loads + * @param loadFailureCount - Number of failed cache loads + * @param totalLoadTime - Total load time in milliseconds + * @param evictionCount - Number of cache evictions + */ + static of( + hitCount = 0, + missCount = 0, + loadSuccessCount = 0, + loadFailureCount = 0, + totalLoadTime = 0, + evictionCount = 0 + ): CacheStats { + return new CacheStats( + hitCount, + missCount, + loadSuccessCount, + loadFailureCount, + totalLoadTime, + evictionCount + ); + } + + /** + * Returns a statistics instance where no cache events have been recorded. + * + * @returns An empty statistics instance + */ + static empty(): CacheStats { + return CacheStats.EMPTY_STATS; + } + + /** + * An empty stats instance with all counters set to zero. + */ + private static readonly EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0); + + /** + * Returns the total number of times cache lookup methods have returned + * either a cached or uncached value. + * + * @returns Total number of requests (hits + misses) + */ + requestCount(): number { + return this.hitCount + this.missCount; + } + + /** + * Returns the hit rate of the cache. + * This is defined as hitCount / requestCount, or 1.0 when requestCount is 0. + * + * @returns The ratio of cache requests that were hits (between 0.0 and 1.0) + */ + hitRate(): number { + const requestCount = this.requestCount(); + return requestCount === 0 ? 1.0 : this.hitCount / requestCount; + } + + /** + * Returns the miss rate of the cache. + * This is defined as missCount / requestCount, or 0.0 when requestCount is 0. + * + * @returns The ratio of cache requests that were misses (between 0.0 and 1.0) + */ + missRate(): number { + const requestCount = this.requestCount(); + return requestCount === 0 ? 0.0 : this.missCount / requestCount; + } + + /** + * Returns the total number of load operations (successful + failed). + * + * @returns Total number of load operations + */ + loadCount(): number { + return this.loadSuccessCount + this.loadFailureCount; + } + + /** + * Returns the ratio of cache loading attempts that failed. + * This is defined as loadFailureCount / loadCount, or 0.0 when loadCount is 0. + * + * @returns Ratio of load operations that failed (between 0.0 and 1.0) + */ + loadFailureRate(): number { + const loadCount = this.loadCount(); + return loadCount === 0 ? 0.0 : this.loadFailureCount / loadCount; + } + + /** + * Returns the average time spent loading new values, in milliseconds. + * This is defined as totalLoadTime / loadCount, or 0.0 when loadCount is 0. + * + * @returns Average load time in milliseconds + */ + averageLoadPenalty(): number { + const loadCount = this.loadCount(); + return loadCount === 0 ? 0.0 : this.totalLoadTime / loadCount; + } + + /** + * Returns a new CacheStats representing the difference between this CacheStats + * and another. Negative values are rounded up to zero. + * + * @param other - The statistics to subtract from this instance + * @returns The difference between this instance and other + */ + minus(other: CacheStats): CacheStats { + return CacheStats.of( + Math.max(0, this.hitCount - other.hitCount), + Math.max(0, this.missCount - other.missCount), + Math.max(0, this.loadSuccessCount - other.loadSuccessCount), + Math.max(0, this.loadFailureCount - other.loadFailureCount), + Math.max(0, this.totalLoadTime - other.totalLoadTime), + Math.max(0, this.evictionCount - other.evictionCount) + ); + } + + /** + * Returns a new CacheStats representing the sum of this CacheStats and another. + * + * @param other - The statistics to add to this instance + * @returns The sum of this instance and other + */ + plus(other: CacheStats): CacheStats { + return CacheStats.of( + this.hitCount + other.hitCount, + this.missCount + other.missCount, + this.loadSuccessCount + other.loadSuccessCount, + this.loadFailureCount + other.loadFailureCount, + this.totalLoadTime + other.totalLoadTime, + this.evictionCount + other.evictionCount + ); + } +} + +/** + * An accumulator for cache statistics. + * + * This interface defines the contract for objects that record cache-related events + * such as hits, misses, loads (successes and failures), and evictions. The design + * is inspired by the statistics collection mechanisms in Ben Manes's Caffeine cache + * (https://github.com/ben-manes/caffeine). + * + * Implementations of this interface are responsible for aggregating these events. + * A snapshot of the current statistics can be obtained by calling the `snapshot()` + * method, which returns an immutable {@link CacheStats} object. + * + * Common implementations include `DefaultStatsCounter` for active statistics collection + * and `DisabledStatsCounter` for a no-op version when stats are not needed. + */ +export interface StatsCounter { + /** + * Records cache hits. This should be called when a cache request returns a cached value. + * + * @param count - The number of hits to record + */ + recordHits(count: number): void; + + /** + * Records cache misses. This should be called when a cache request returns a value that was not + * found in the cache. + * + * @param count - The number of misses to record + */ + recordMisses(count: number): void; + + /** + * Records the successful load of a new entry. This method should be called when a cache request + * causes an entry to be loaded and the loading completes successfully. + * + * @param loadTime - The number of milliseconds the cache spent computing or retrieving the new value + */ + recordLoadSuccess(loadTime: number): void; + + /** + * Records the failed load of a new entry. This method should be called when a cache request + * causes an entry to be loaded, but an exception is thrown while loading the entry. + * + * @param loadTime - The number of milliseconds the cache spent computing or retrieving the new value + * prior to the failure + */ + recordLoadFailure(loadTime: number): void; + + /** + * Records the eviction of an entry from the cache. This should only be called when an entry is + * evicted due to the cache's eviction strategy, and not as a result of manual invalidations. + * + * @param count - The number of evictions to record + */ + recordEvictions(count: number): void; + + /** + * Returns a snapshot of this counter's values. Note that this may be an inconsistent view, as it + * may be interleaved with update operations. + * + * @return A snapshot of this counter's values + */ + snapshot(): CacheStats; +} + +/** + * A StatsCounter implementation that does nothing and always returns empty stats. + */ +class DisabledStatsCounter implements StatsCounter { + static readonly INSTANCE = new DisabledStatsCounter(); + + private constructor() { } + + recordHits(count: number): void { } + recordMisses(count: number): void { } + recordLoadSuccess(loadTime: number): void { } + recordLoadFailure(loadTime: number): void { } + recordEvictions(count: number): void { } + snapshot(): CacheStats { return CacheStats.empty(); } +} + +/** + * Returns a StatsCounter that does not record any cache events. + * + * @return A StatsCounter that does not record metrics + */ +function disabledStatsCounter(): StatsCounter { + return DisabledStatsCounter.INSTANCE; +} + +/** + * A StatsCounter implementation that maintains cache statistics. + */ +class DefaultStatsCounter implements StatsCounter { + #hitCount = 0; + #missCount = 0; + #loadSuccessCount = 0; + #loadFailureCount = 0; + #totalLoadTime = 0; + #evictionCount = 0; + + /** + * Records cache hits. + * + * @param count - The number of hits to record + */ + recordHits(count: number): void { + this.#hitCount += count; + } + + /** + * Records cache misses. + * + * @param count - The number of misses to record + */ + recordMisses(count: number): void { + this.#missCount += count; + } + + /** + * Records the successful load of a new entry. + * + * @param loadTime - The number of milliseconds spent loading the entry + */ + recordLoadSuccess(loadTime: number): void { + this.#loadSuccessCount++; + this.#totalLoadTime += loadTime; + } + + /** + * Records the failed load of a new entry. + * + * @param loadTime - The number of milliseconds spent attempting to load the entry + */ + recordLoadFailure(loadTime: number): void { + this.#loadFailureCount++; + this.#totalLoadTime += loadTime; + } + + /** + * Records cache evictions. + * + * @param count - The number of evictions to record + */ + recordEvictions(count: number): void { + this.#evictionCount += count; + } + + /** + * Returns a snapshot of the current statistics. + * + * @returns A snapshot of the current statistics + */ + snapshot(): CacheStats { + return CacheStats.of( + this.#hitCount, + this.#missCount, + this.#loadSuccessCount, + this.#loadFailureCount, + this.#totalLoadTime, + this.#evictionCount + ); + } + + /** + * Creates a new DefaultStatsCounter. + * + * @returns A new DefaultStatsCounter instance + */ + static create(): DefaultStatsCounter { + return new DefaultStatsCounter(); + } +} + +type CachingClient = RedisClient; +type CmdFunc = () => Promise; + +type EvictionPolicy = "LRU" | "FIFO" + +/** + * Configuration options for Client Side Cache + */ +export interface ClientSideCacheConfig { + /** + * Time-to-live in milliseconds for cached entries. + * Use 0 for no expiration. + * @default 0 + */ + ttl?: number; + + /** + * Maximum number of entries to store in the cache. + * Use 0 for unlimited entries. + * @default 0 + */ + maxEntries?: number; + + /** + * Eviction policy to use when the cache reaches its capacity. + * - "LRU" (Least Recently Used): Evicts least recently accessed entries first + * - "FIFO" (First In First Out): Evicts oldest entries first + * @default "LRU" + */ + evictPolicy?: EvictionPolicy; + + /** + * Whether to collect statistics about cache operations. + * @default true + */ + recordStats?: boolean; +} + +interface CacheCreator { + epoch: number; + client: CachingClient; +} + +interface ClientSideCacheEntry { + invalidate(): void; + validate(): boolean; +} + +/** + * Generates a unique cache key from Redis command arguments + * + * @param redisArgs - Array of Redis command arguments + * @returns A unique string key for caching + */ +function generateCacheKey(redisArgs: ReadonlyArray): string { + const tmp = new Array(redisArgs.length * 2); + + for (let i = 0; i < redisArgs.length; i++) { + tmp[i] = redisArgs[i].length; + tmp[i + redisArgs.length] = redisArgs[i]; + } + + return tmp.join('_'); +} + +abstract class ClientSideCacheEntryBase implements ClientSideCacheEntry { + #invalidated = false; + readonly #expireTime: number; + + constructor(ttl: number) { + if (ttl == 0) { + this.#expireTime = 0; + } else { + this.#expireTime = Date.now() + ttl; + } + } + + invalidate(): void { + this.#invalidated = true; + } + + validate(): boolean { + return !this.#invalidated && (this.#expireTime == 0 || (Date.now() < this.#expireTime)) + } +} + +class ClientSideCacheEntryValue extends ClientSideCacheEntryBase { + readonly #value: any; + + get value() { + return this.#value; + } + + constructor(ttl: number, value: any) { + super(ttl); + this.#value = value; + } +} + +class ClientSideCacheEntryPromise extends ClientSideCacheEntryBase { + readonly #sendCommandPromise: Promise; + + get promise() { + return this.#sendCommandPromise; + } + + constructor(ttl: number, sendCommandPromise: Promise) { + super(ttl); + this.#sendCommandPromise = sendCommandPromise; + } +} + +export abstract class ClientSideCacheProvider extends EventEmitter { + abstract handleCache(client: CachingClient, parser: BasicCommandParser, fn: CmdFunc, transformReply: TransformReply | undefined, typeMapping: TypeMapping | undefined): Promise; + abstract trackingOn(): Array; + abstract invalidate(key: RedisArgument | null): void; + abstract clear(): void; + abstract stats(): CacheStats; + abstract onError(): void; + abstract onClose(): void; +} + +export class BasicClientSideCache extends ClientSideCacheProvider { + #cacheKeyToEntryMap: Map; + #keyToCacheKeySetMap: Map>; + readonly ttl: number; + readonly maxEntries: number; + readonly lru: boolean; + #statsCounter: StatsCounter; + + + recordEvictions(count: number): void { + this.#statsCounter.recordEvictions(count); + } + + recordHits(count: number): void { + this.#statsCounter.recordHits(count); + } + + recordMisses(count: number): void { + this.#statsCounter.recordMisses(count); + } + + constructor(config?: ClientSideCacheConfig) { + super(); + + this.#cacheKeyToEntryMap = new Map(); + this.#keyToCacheKeySetMap = new Map>(); + this.ttl = config?.ttl ?? 0; + this.maxEntries = config?.maxEntries ?? 0; + this.lru = config?.evictPolicy !== "FIFO"; + + const recordStats = config?.recordStats !== false; + this.#statsCounter = recordStats ? DefaultStatsCounter.create() : disabledStatsCounter(); + } + + /* logic of how caching works: + + 1. commands use a CommandParser + it enables us to define/retrieve + cacheKey - a unique key that corresponds to this command and its arguments + redisKeys - an array of redis keys as strings that if the key is modified, will cause redis to invalidate this result when cached + 2. check if cacheKey is in our cache + 2b1. if its a value cacheEntry - return it + 2b2. if it's a promise cache entry - wait on promise and then go to 3c. + 3. if cacheEntry is not in cache + 3a. send the command save the promise into a a cacheEntry and then wait on result + 3b. transform reply (if required) based on transformReply + 3b. check the cacheEntry is still valid - in cache and hasn't been deleted) + 3c. if valid - overwrite with value entry + 4. return previously non cached result + */ + override async handleCache( + client: CachingClient, + parser: BasicCommandParser, + fn: CmdFunc, + transformReply?: TransformReply, + typeMapping?: TypeMapping + ) { + let reply: ReplyUnion; + + const cacheKey = generateCacheKey(parser.redisArgs); + + // "2" + let cacheEntry = this.get(cacheKey); + if (cacheEntry) { + // If instanceof is "too slow", can add a "type" and then use an "as" cast to call proper getters. + if (cacheEntry instanceof ClientSideCacheEntryValue) { // "2b1" + this.#statsCounter.recordHits(1); + + return structuredClone(cacheEntry.value); + } else if (cacheEntry instanceof ClientSideCacheEntryPromise) { // 2b2 + // This counts as a miss since the value hasn't been fully loaded yet. + this.#statsCounter.recordMisses(1); + reply = await cacheEntry.promise; + } else { + throw new Error("unknown cache entry type"); + } + } else { // 3/3a + this.#statsCounter.recordMisses(1); + + const startTime = performance.now(); + const promise = fn(); + + cacheEntry = this.createPromiseEntry(client, promise); + this.set(cacheKey, cacheEntry, parser.keys); + + try { + reply = await promise; + const loadTime = performance.now() - startTime; + this.#statsCounter.recordLoadSuccess(loadTime); + } catch (err) { + const loadTime = performance.now() - startTime; + this.#statsCounter.recordLoadFailure(loadTime); + + if (cacheEntry.validate()) { + this.delete(cacheKey!); + } + + throw err; + } + } + + // 3b + let val; + if (transformReply) { + val = transformReply(reply, parser.preserve, typeMapping); + } else { + val = reply; + } + + // 3c + if (cacheEntry.validate()) { // revalidating promise entry (dont save value, if promise entry has been invalidated) + // 3d + cacheEntry = this.createValueEntry(client, val); + this.set(cacheKey, cacheEntry, parser.keys); + this.emit("cached-key", cacheKey); + } else { + // cache entry for key got invalidated between execution and saving, so not saving + } + + return structuredClone(val); + } + + override trackingOn() { + return ['CLIENT', 'TRACKING', 'ON']; + } + + override invalidate(key: RedisArgument | null) { + if (key === null) { + this.clear(false); + this.emit("invalidate", key); + + return; + } + + const keySet = this.#keyToCacheKeySetMap.get(key.toString()); + if (keySet) { + for (const cacheKey of keySet) { + const entry = this.#cacheKeyToEntryMap.get(cacheKey); + if (entry) { + entry.invalidate(); + } + this.#cacheKeyToEntryMap.delete(cacheKey); + } + this.#keyToCacheKeySetMap.delete(key.toString()); + } + + this.emit('invalidate', key); + } + + override clear(resetStats = true) { + const oldSize = this.#cacheKeyToEntryMap.size; + this.#cacheKeyToEntryMap.clear(); + this.#keyToCacheKeySetMap.clear(); + + if (resetStats) { + if (!(this.#statsCounter instanceof DisabledStatsCounter)) { + this.#statsCounter = DefaultStatsCounter.create(); + } + } else { + // If old entries were evicted due to clear, record them as evictions + if (oldSize > 0) { + this.#statsCounter.recordEvictions(oldSize); + } + } + } + + get(cacheKey: string) { + const val = this.#cacheKeyToEntryMap.get(cacheKey); + + if (val && !val.validate()) { + this.delete(cacheKey); + this.#statsCounter.recordEvictions(1); + this.emit("cache-evict", cacheKey); + + return undefined; + } + + if (val !== undefined && this.lru) { + this.#cacheKeyToEntryMap.delete(cacheKey); + this.#cacheKeyToEntryMap.set(cacheKey, val); + } + + return val; + } + + delete(cacheKey: string) { + const entry = this.#cacheKeyToEntryMap.get(cacheKey); + if (entry) { + entry.invalidate(); + this.#cacheKeyToEntryMap.delete(cacheKey); + } + } + + has(cacheKey: string) { + return this.#cacheKeyToEntryMap.has(cacheKey); + } + + set(cacheKey: string, cacheEntry: ClientSideCacheEntry, keys: Array) { + let count = this.#cacheKeyToEntryMap.size; + const oldEntry = this.#cacheKeyToEntryMap.get(cacheKey); + + if (oldEntry) { + count--; // overwriting, so not incrementig + oldEntry.invalidate(); + } + + if (this.maxEntries > 0 && count >= this.maxEntries) { + this.deleteOldest(); + this.#statsCounter.recordEvictions(1); + } + + this.#cacheKeyToEntryMap.set(cacheKey, cacheEntry); + + for (const key of keys) { + if (!this.#keyToCacheKeySetMap.has(key.toString())) { + this.#keyToCacheKeySetMap.set(key.toString(), new Set()); + } + + const cacheKeySet = this.#keyToCacheKeySetMap.get(key.toString()); + cacheKeySet!.add(cacheKey); + } + } + + size() { + return this.#cacheKeyToEntryMap.size; + } + + createValueEntry(client: CachingClient, value: any): ClientSideCacheEntryValue { + return new ClientSideCacheEntryValue(this.ttl, value); + } + + createPromiseEntry(client: CachingClient, sendCommandPromise: Promise): ClientSideCacheEntryPromise { + return new ClientSideCacheEntryPromise(this.ttl, sendCommandPromise); + } + + override stats(): CacheStats { + return this.#statsCounter.snapshot(); + } + + override onError(): void { + this.clear(); + } + + override onClose() { + this.clear(); + } + + /** + * @internal + */ + deleteOldest() { + const it = this.#cacheKeyToEntryMap[Symbol.iterator](); + const n = it.next(); + if (!n.done) { + const key = n.value[0]; + const entry = this.#cacheKeyToEntryMap.get(key); + if (entry) { + entry.invalidate(); + } + this.#cacheKeyToEntryMap.delete(key); + } + } + + /** + * Get cache entries for debugging + * @internal + */ + entryEntries(): IterableIterator<[string, ClientSideCacheEntry]> { + return this.#cacheKeyToEntryMap.entries(); + } + + /** + * Get key set entries for debugging + * @internal + */ + keySetEntries(): IterableIterator<[string, Set]> { + return this.#keyToCacheKeySetMap.entries(); + } +} + +export abstract class PooledClientSideCacheProvider extends BasicClientSideCache { + #disabled = false; + + disable(): void { + this.#disabled = true; + } + + enable(): void { + this.#disabled = false; + } + + override get(cacheKey: string): ClientSideCacheEntry | undefined { + if (this.#disabled) { + return undefined; + } + + return super.get(cacheKey); + } + + override has(cacheKey: string): boolean { + if (this.#disabled) { + return false; + } + + return super.has(cacheKey); + } + + onPoolClose(): void { + this.clear(); + } +} + +export class BasicPooledClientSideCache extends PooledClientSideCacheProvider { + override onError() { + this.clear(false); + } + + override onClose() { + this.clear(false); + } +} + +class PooledClientSideCacheEntryValue extends ClientSideCacheEntryValue { + #creator: CacheCreator; + + constructor(ttl: number, creator: CacheCreator, value: any) { + super(ttl, value); + + this.#creator = creator; + } + + override validate(): boolean { + let ret = super.validate(); + if (this.#creator) { + ret = ret && this.#creator.client.isReady && this.#creator.client.socketEpoch == this.#creator.epoch + } + + return ret; + } +} + +class PooledClientSideCacheEntryPromise extends ClientSideCacheEntryPromise { + #creator: CacheCreator; + + constructor(ttl: number, creator: CacheCreator, sendCommandPromise: Promise) { + super(ttl, sendCommandPromise); + + this.#creator = creator; + } + + override validate(): boolean { + let ret = super.validate(); + + return ret && this.#creator.client.isReady && this.#creator.client.socketEpoch == this.#creator.epoch + } +} + +export class PooledNoRedirectClientSideCache extends BasicPooledClientSideCache { + override createValueEntry(client: CachingClient, value: any): ClientSideCacheEntryValue { + const creator = { + epoch: client.socketEpoch, + client: client + }; + + return new PooledClientSideCacheEntryValue(this.ttl, creator, value); + } + + override createPromiseEntry(client: CachingClient, sendCommandPromise: Promise): ClientSideCacheEntryPromise { + const creator = { + epoch: client.socketEpoch, + client: client + }; + + return new PooledClientSideCacheEntryPromise(this.ttl, creator, sendCommandPromise); + } + + override onError() { } + + override onClose() { } +} diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index 15e8a747b98..78c0a01b203 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -56,6 +56,8 @@ export default class RedisCommandsQueue { return this.#pubSub.isActive; } + #invalidateCallback?: (key: RedisArgument | null) => unknown; + constructor( respVersion: RespVersions, maxLength: number | null | undefined, @@ -107,15 +109,34 @@ export default class RedisCommandsQueue { return new Decoder({ onReply: reply => this.#onReply(reply), onErrorReply: err => this.#onErrorReply(err), + //TODO: we can shave off a few cycles by not adding onPush handler at all if CSC is not used onPush: push => { if (!this.#onPush(push)) { - + // currently only supporting "invalidate" over RESP3 push messages + switch (push[0].toString()) { + case "invalidate": { + if (this.#invalidateCallback) { + if (push[1] !== null) { + for (const key of push[1]) { + this.#invalidateCallback(key); + } + } else { + this.#invalidateCallback(null); + } + } + break; + } + } } }, getTypeMapping: () => this.#getTypeMapping() }); } + setInvalidateCallback(callback?: (key: RedisArgument | null) => unknown) { + this.#invalidateCallback = callback; + } + addCommand( args: ReadonlyArray, options?: CommandOptions diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index c71cf1a1fad..cc052dd5b51 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -24,10 +24,44 @@ export const SQUARE_SCRIPT = defineScript({ }); describe('Client', () => { + describe('initialization', () => { + describe('clientSideCache validation', () => { + const clientSideCacheConfig = { ttl: 0, maxEntries: 0 }; + + it('should throw error when clientSideCache is enabled with RESP 2', () => { + assert.throws( + () => new RedisClient({ + clientSideCache: clientSideCacheConfig, + RESP: 2, + }), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should throw error when clientSideCache is enabled with RESP undefined', () => { + assert.throws( + () => new RedisClient({ + clientSideCache: clientSideCacheConfig, + }), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should not throw when clientSideCache is enabled with RESP 3', () => { + assert.doesNotThrow(() => + new RedisClient({ + clientSideCache: clientSideCacheConfig, + RESP: 3, + }) + ); + }); + }); + }); + describe('parseURL', () => { it('redis://user:secret@localhost:6379/0', async () => { const result = RedisClient.parseURL('redis://user:secret@localhost:6379/0'); - const expected : RedisClientOptions = { + const expected: RedisClientOptions = { socket: { host: 'localhost', port: 6379 @@ -51,8 +85,8 @@ describe('Client', () => { // Compare non-function properties assert.deepEqual(resultRest, expectedRest); - if(result.credentialsProvider.type === 'async-credentials-provider' - && expected.credentialsProvider.type === 'async-credentials-provider') { + 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(); @@ -91,10 +125,10 @@ describe('Client', () => { // Compare non-function properties assert.deepEqual(resultRest, expectedRest); - assert.equal(resultCredProvider.type, expectedCredProvider.type); + assert.equal(resultCredProvider?.type, expectedCredProvider?.type); - if (result.credentialsProvider.type === 'async-credentials-provider' && - expected.credentialsProvider.type === 'async-credentials-provider') { + 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(); @@ -150,11 +184,11 @@ describe('Client', () => { 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); + assert.equal( + await client.ping(), + 'PONG' + ); + }, GLOBAL.SERVERS.STREAMING_AUTH); testUtils.testWithClient('should execute AUTH before SELECT', async client => { assert.equal( @@ -408,7 +442,7 @@ describe('Client', () => { }); testUtils.testWithClient('functions', async client => { - const [,, reply] = await Promise.all([ + const [, , reply] = await Promise.all([ loadMathFunction(client), client.set('key', '2'), client.math.square('key') @@ -522,8 +556,8 @@ describe('Client', () => { const hash: Record = {}; const expectedFields: Array = []; for (let i = 0; i < 100; i++) { - hash[i.toString()] = i.toString(); - expectedFields.push(i.toString()); + hash[i.toString()] = i.toString(); + expectedFields.push(i.toString()); } await client.hSet('key', hash); @@ -842,7 +876,7 @@ describe('Client', () => { testUtils.testWithClient('should be able to go back to "normal mode"', async client => { await Promise.all([ - client.monitor(() => {}), + client.monitor(() => { }), client.reset() ]); await assert.doesNotReject(client.ping()); diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index f3e72a3a172..c7f94fe680a 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -16,6 +16,7 @@ import { ScanOptions, ScanCommonOptions } from '../commands/SCAN'; import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; import { RedisPoolOptions, RedisClientPool } from './pool'; import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../commands/generic-transformers'; +import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache'; import { BasicCommandParser, CommandParser } from './parser'; import SingleEntryCache from '../single-entry-cache'; @@ -78,45 +79,98 @@ export interface RedisClientOptions< */ pingInterval?: number; /** - * TODO + * Default command options to be applied to all commands executed through this client. + * + * These options can be overridden on a per-command basis when calling specific commands. + * + * @property {symbol} [chainId] - Identifier for chaining commands together + * @property {boolean} [asap] - When true, the command is executed as soon as possible + * @property {AbortSignal} [abortSignal] - AbortSignal to cancel the command + * @property {TypeMapping} [typeMapping] - Custom type mappings between RESP and JavaScript types + * + * @example Setting default command options + * ``` + * const client = createClient({ + * commandOptions: { + * asap: true, + * typeMapping: { + * // Custom type mapping configuration + * } + * } + * }); + * ``` */ commandOptions?: CommandOptions; + /** + * Client Side Caching configuration. + * + * Enables Redis Servers and Clients to work together to cache results from commands + * sent to a server. The server will notify the client when cached results are no longer valid. + * + * Note: Client Side Caching is only supported with RESP3. + * + * @example Anonymous cache configuration + * ``` + * const client = createClient({ + * RESP: 3, + * clientSideCache: { + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * } + * }); + * ``` + * + * @example Using a controllable cache + * ``` + * const cache = new BasicClientSideCache({ + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }); + * const client = createClient({ + * RESP: 3, + * clientSideCache: cache + * }); + * ``` + */ + clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig; } type WithCommands< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>; -}; + [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; + [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; + [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; -}; + [P in keyof S]: CommandSignature; + }; export type RedisClientExtensions< M extends RedisModules = {}, @@ -125,11 +179,11 @@ export type RedisClientExtensions< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} > = ( - WithCommands & - WithModules & - WithFunctions & - WithScripts -); + WithCommands & + WithModules & + WithFunctions & + WithScripts + ); export type RedisClientType< M extends RedisModules = {}, @@ -138,9 +192,9 @@ export type RedisClientType< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} > = ( - RedisClient & - RedisClientExtensions -); + RedisClient & + RedisClientExtensions + ); type ProxyClient = RedisClient; @@ -309,14 +363,17 @@ 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 + // flag used to annotate that the client + // was in a watch transaction when // a topology change occured #dirtyWatch?: string; - #epoch: number; - #watchEpoch?: number; - + #watchEpoch?: number; + #clientSideCache?: ClientSideCacheProvider; #credentialsSubscription: Disposable | null = null; + get clientSideCache() { + return this._self.#clientSideCache; + } + get options(): RedisClientOptions | undefined { return this._self.#options; @@ -334,6 +391,10 @@ export default class RedisClient< return this._self.#queue.isPubSubActive; } + get socketEpoch() { + return this._self.#socket.socketEpoch; + } + get isWatching() { return this._self.#watchEpoch !== undefined; } @@ -358,12 +419,28 @@ export default class RedisClient< constructor(options?: RedisClientOptions) { super(); + this.#validateOptions(options) this.#options = this.#initiateOptions(options); this.#queue = this.#initiateQueue(); this.#socket = this.#initiateSocket(); - this.#epoch = 0; + + if (options?.clientSideCache) { + if (options.clientSideCache instanceof ClientSideCacheProvider) { + this.#clientSideCache = options.clientSideCache; + } else { + const cscConfig = options.clientSideCache; + this.#clientSideCache = new BasicClientSideCache(cscConfig); + } + this.#queue.setInvalidateCallback(this.#clientSideCache.invalidate.bind(this.#clientSideCache)); + } } + #validateOptions(options?: RedisClientOptions) { + if (options?.clientSideCache && options?.RESP !== 3) { + throw new Error('Client Side Caching is only supported with RESP3'); + } + + } #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { // Convert username/password to credentialsProvider if no credentialsProvider is already in place @@ -421,7 +498,7 @@ export default class RedisClient< } } - #subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> { + #subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> { return cp.subscribe({ onNext: credentials => { this.reAuthenticate(credentials).catch(error => { @@ -456,7 +533,7 @@ export default class RedisClient< if (cp && cp.type === 'streaming-credentials-provider') { - const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) this.#credentialsSubscription = disposable; if (credentials.password) { @@ -492,7 +569,7 @@ export default class RedisClient< if (cp && cp.type === 'streaming-credentials-provider') { - const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) this.#credentialsSubscription = disposable; if (credentials.username || credentials.password) { @@ -522,6 +599,10 @@ export default class RedisClient< ); } + if (this.#clientSideCache) { + commands.push(this.#clientSideCache.trackingOn()); + } + return commands; } @@ -575,6 +656,7 @@ export default class RedisClient< }) .on('error', err => { this.emit('error', err); + this.#clientSideCache?.onError(); if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) { this.#queue.flushWaitingForReply(err); } else { @@ -583,7 +665,6 @@ export default class RedisClient< }) .on('connect', () => this.emit('connect')) .on('ready', () => { - this.#epoch++; this.emit('ready'); this.#setPingTimer(); this.#maybeScheduleWrite(); @@ -711,14 +792,21 @@ export default class RedisClient< commandOptions: CommandOptions | undefined, transformReply: TransformReply | undefined, ) { - const reply = await this.sendCommand(parser.redisArgs, commandOptions); + const csc = this._self.#clientSideCache; + const defaultTypeMapping = this._self.#options?.commandOptions === commandOptions; - if (transformReply) { - const res = transformReply(reply, parser.preserve, commandOptions?.typeMapping); - return res - } + const fn = () => { return this.sendCommand(parser.redisArgs, commandOptions) }; + + if (csc && command.CACHEABLE && defaultTypeMapping) { + return await csc.handleCache(this._self, parser as BasicCommandParser, fn, transformReply, commandOptions?.typeMapping); + } else { + const reply = await fn(); - return reply; + if (transformReply) { + return transformReply(reply, parser.preserve, commandOptions?.typeMapping); + } + return reply; + } } /** @@ -883,7 +971,7 @@ export default class RedisClient< const reply = await this._self.sendCommand( pushVariadicArguments(['WATCH'], key) ); - this._self.#watchEpoch ??= this._self.#epoch; + this._self.#watchEpoch ??= this._self.socketEpoch; return reply as unknown as ReplyWithTypeMapping, TYPE_MAPPING>; } @@ -942,7 +1030,7 @@ export default class RedisClient< * @internal */ async _executePipeline( - commands: Array, + commands: Array, selectedDB?: number ) { if (!this._self.#socket.isOpen) { @@ -986,15 +1074,15 @@ export default class RedisClient< throw new WatchError(dirtyWatch); } - if (watchEpoch && watchEpoch !== this._self.#epoch) { + if (watchEpoch && watchEpoch !== this._self.socketEpoch) { throw new WatchError('Client reconnected after WATCH'); } const typeMapping = this._commandOptions?.typeMapping; const chainId = Symbol('MULTI Chain'); const promises = [ - this._self.#queue.addCommand(['MULTI'], { chainId }), - ]; + this._self.#queue.addCommand(['MULTI'], { chainId }), + ]; for (const { args } of commands) { promises.push( @@ -1210,6 +1298,7 @@ export default class RedisClient< return new Promise(resolve => { clearTimeout(this._self.#pingTimer); this._self.#socket.close(); + this._self.#clientSideCache?.onClose(); if (this._self.#queue.isEmpty()) { this._self.#socket.destroySocket(); @@ -1236,6 +1325,7 @@ export default class RedisClient< clearTimeout(this._self.#pingTimer); this._self.#queue.flushAll(new DisconnectsClientError()); this._self.#socket.destroy(); + this._self.#clientSideCache?.onClose(); this._self.#credentialsSubscription?.dispose(); this._self.#credentialsSubscription = null; } diff --git a/packages/client/lib/client/linked-list.ts b/packages/client/lib/client/linked-list.ts index ac1d021be91..29678f027b5 100644 --- a/packages/client/lib/client/linked-list.ts +++ b/packages/client/lib/client/linked-list.ts @@ -114,6 +114,7 @@ export class DoublyLinkedList { export interface SinglyLinkedNode { value: T; next: SinglyLinkedNode | undefined; + removed: boolean; } export class SinglyLinkedList { @@ -140,7 +141,8 @@ export class SinglyLinkedList { const node = { value, - next: undefined + next: undefined, + removed: false }; if (this.#head === undefined) { @@ -151,6 +153,9 @@ export class SinglyLinkedList { } remove(node: SinglyLinkedNode, parent: SinglyLinkedNode | undefined) { + if (node.removed) { + throw new Error("node already removed"); + } --this.#length; if (this.#head === node) { @@ -165,6 +170,8 @@ export class SinglyLinkedList { } else { parent!.next = node.next; } + + node.removed = true; } shift() { @@ -177,6 +184,7 @@ export class SinglyLinkedList { this.#head = node.next; } + node.removed = true; return node.value; } diff --git a/packages/client/lib/client/parser.ts b/packages/client/lib/client/parser.ts index 12eec457739..3e820230429 100644 --- a/packages/client/lib/client/parser.ts +++ b/packages/client/lib/client/parser.ts @@ -33,6 +33,17 @@ export class BasicCommandParser implements CommandParser { return this.#keys[0]; } + get cacheKey() { + const tmp = new Array(this.#redisArgs.length*2); + + for (let i = 0; i < this.#redisArgs.length; i++) { + tmp[i] = this.#redisArgs[i].length; + tmp[i+this.#redisArgs.length] = this.#redisArgs[i]; + } + + return tmp.join('_'); + } + push(...arg: Array) { this.#redisArgs.push(...arg); }; diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index ec89f0c39e3..6f633c9caa7 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 { BasicPooledClientSideCache, ClientSideCacheConfig, PooledClientSideCacheProvider } from './cache'; import { BasicCommandParser } from './parser'; import SingleEntryCache from '../single-entry-cache'; @@ -24,11 +25,55 @@ export interface RedisPoolOptions { */ acquireTimeout: number; /** - * TODO + * The delay in milliseconds before a cleanup operation is performed on idle clients. + * + * After this delay, the pool will check if there are too many idle clients and destroy + * excess ones to maintain optimal pool size. */ cleanupDelay: number; /** - * TODO + * Client Side Caching configuration for the pool. + * + * Enables Redis Servers and Clients to work together to cache results from commands + * sent to a server. The server will notify the client when cached results are no longer valid. + * In pooled mode, the cache is shared across all clients in the pool. + * + * Note: Client Side Caching is only supported with RESP3. + * + * @example Anonymous cache configuration + * ``` + * const client = createClientPool({RESP: 3}, { + * clientSideCache: { + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }, + * minimum: 5 + * }); + * ``` + * + * @example Using a controllable cache + * ``` + * const cache = new BasicPooledClientSideCache({ + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }); + * const client = createClientPool({RESP: 3}, { + * clientSideCache: cache, + * minimum: 5 + * }); + * ``` + */ + clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig; + /** + * Enable experimental support for RESP3 module commands. + * + * When enabled, allows the use of module commands that have been adapted + * for the RESP3 protocol. This is an unstable feature and may change in + * future versions. + * + * @default false */ unstableResp3Modules?: boolean; } @@ -120,7 +165,7 @@ export class RedisClientPool< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping = {} >( - clientOptions?: RedisClientOptions, + clientOptions?: Omit, "clientSideCache">, options?: Partial ) { @@ -142,7 +187,7 @@ export class RedisClientPool< // returning a "proxy" to prevent the namespaces._self to leak between "proxies" return Object.create( new Pool( - RedisClient.factory(clientOptions).bind(undefined, clientOptions), + clientOptions, options ) ) as RedisClientPoolType; @@ -216,22 +261,41 @@ export class RedisClientPool< return this._self.#isClosing; } + #clientSideCache?: PooledClientSideCacheProvider; + get clientSideCache() { + return this._self.#clientSideCache; + } + /** * You are probably looking for {@link RedisClient.createPool `RedisClient.createPool`}, * {@link RedisClientPool.fromClient `RedisClientPool.fromClient`}, * or {@link RedisClientPool.fromOptions `RedisClientPool.fromOptions`}... */ constructor( - clientFactory: () => RedisClientType, + clientOptions?: RedisClientOptions, options?: Partial ) { super(); - this.#clientFactory = clientFactory; this.#options = { ...RedisClientPool.#DEFAULTS, ...options }; + if (options?.clientSideCache) { + if (clientOptions === undefined) { + clientOptions = {}; + } + + if (options.clientSideCache instanceof PooledClientSideCacheProvider) { + this.#clientSideCache = clientOptions.clientSideCache = options.clientSideCache; + } else { + const cscConfig = options.clientSideCache; + this.#clientSideCache = clientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig); +// this.#clientSideCache = clientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); + } + } + + this.#clientFactory = RedisClient.factory(clientOptions).bind(undefined, clientOptions) as () => RedisClientType; } private _self = this; @@ -295,7 +359,6 @@ export class RedisClientPool< async connect() { if (this._self.#isOpen) return; // TODO: throw error? - this._self.#isOpen = true; const promises = []; @@ -305,11 +368,12 @@ export class RedisClientPool< try { await Promise.all(promises); - return this as unknown as RedisClientPoolType; } catch (err) { this.destroy(); throw err; } + + return this as unknown as RedisClientPoolType; } async #create() { @@ -319,7 +383,8 @@ export class RedisClientPool< ); try { - await node.value.connect(); + const client = node.value; + await client.connect(); } catch (err) { this._self.#clientsInUse.remove(node); throw err; @@ -408,7 +473,8 @@ export class RedisClientPool< 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(); + const client = this.#idleClients.shift()! + client.destroy(); } } @@ -446,8 +512,10 @@ export class RedisClientPool< for (const client of this._self.#clientsInUse) { promises.push(client.close()); } - + await Promise.all(promises); + + this.#clientSideCache?.onPoolClose(); this._self.#idleClients.reset(); this._self.#clientsInUse.reset(); @@ -467,6 +535,9 @@ export class RedisClientPool< for (const client of this._self.#clientsInUse) { client.destroy(); } + + this._self.#clientSideCache?.onPoolClose(); + this._self.#clientsInUse.reset(); this._self.#isOpen = false; diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts index 36afa36c04a..603416cf9ed 100644 --- a/packages/client/lib/client/socket.ts +++ b/packages/client/lib/client/socket.ts @@ -72,6 +72,12 @@ export default class RedisSocket extends EventEmitter { #isSocketUnrefed = false; + #socketEpoch = 0; + + get socketEpoch() { + return this.#socketEpoch; + } + constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) { super(); @@ -212,6 +218,7 @@ export default class RedisSocket extends EventEmitter { throw err; } this.#isReady = true; + this.#socketEpoch++; this.emit('ready'); } catch (err) { const retryIn = this.#shouldReconnect(retries++, err as Error); diff --git a/packages/client/lib/cluster/cluster-slots.spec.ts b/packages/client/lib/cluster/cluster-slots.spec.ts new file mode 100644 index 00000000000..bea1453037a --- /dev/null +++ b/packages/client/lib/cluster/cluster-slots.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'node:assert'; +import { EventEmitter } from 'node:events'; +import { RedisClusterOptions, RedisClusterClientOptions } from './index'; +import RedisClusterSlots from './cluster-slots'; + +describe('RedisClusterSlots', () => { + describe('initialization', () => { + + describe('clientSideCache validation', () => { + const mockEmit = ((_event: string | symbol, ..._args: any[]): boolean => true) as EventEmitter['emit']; + const clientSideCacheConfig = { ttl: 0, maxEntries: 0 }; + const rootNodes: Array = [ + { socket: { host: 'localhost', port: 30001 } } + ]; + + it('should throw error when clientSideCache is enabled with RESP 2', () => { + assert.throws( + () => new RedisClusterSlots({ + rootNodes, + clientSideCache: clientSideCacheConfig, + RESP: 2 as const, + }, mockEmit), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should throw error when clientSideCache is enabled with RESP undefined', () => { + assert.throws( + () => new RedisClusterSlots({ + rootNodes, + clientSideCache: clientSideCacheConfig, + }, mockEmit), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should not throw when clientSideCache is enabled with RESP 3', () => { + assert.doesNotThrow(() => + new RedisClusterSlots({ + rootNodes, + clientSideCache: clientSideCacheConfig, + RESP: 3 as const, + }, mockEmit) + ); + }); + }); + }); +}); diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 0679b200349..84486112320 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -6,6 +6,7 @@ import { ChannelListeners, PUBSUB_TYPE, PubSubTypeListeners } from '../client/pu import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; import calculateSlot from 'cluster-key-slot'; import { RedisSocketOptions } from '../client/socket'; +import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache'; interface NodeAddress { host: string; @@ -111,6 +112,7 @@ export default class RedisClusterSlots< replicas = new Array>(); readonly nodeByAddress = new Map | ShardNode>(); pubSubNode?: PubSubNode; + clientSideCache?: PooledClientSideCacheProvider; #isOpen = false; @@ -118,12 +120,28 @@ export default class RedisClusterSlots< return this.#isOpen; } + #validateOptions(options?: RedisClusterOptions) { + if (options?.clientSideCache && options?.RESP !== 3) { + throw new Error('Client Side Caching is only supported with RESP3'); + } + } + constructor( options: RedisClusterOptions, emit: EventEmitter['emit'] ) { + this.#validateOptions(options); this.#options = options; - this.#clientFactory = RedisClient.factory(options); + + if (options?.clientSideCache) { + if (options.clientSideCache instanceof PooledClientSideCacheProvider) { + this.clientSideCache = options.clientSideCache; + } else { + this.clientSideCache = new BasicPooledClientSideCache(options.clientSideCache) + } + } + + this.#clientFactory = RedisClient.factory(this.#options); this.#emit = emit; } @@ -164,6 +182,8 @@ export default class RedisClusterSlots< } async #discover(rootNode: RedisClusterClientOptions) { + this.clientSideCache?.clear(); + this.clientSideCache?.disable(); try { const addressesInUse = new Set(), promises: Array> = [], @@ -219,6 +239,7 @@ export default class RedisClusterSlots< } await Promise.all(promises); + this.clientSideCache?.enable(); return true; } catch (err) { @@ -314,6 +335,8 @@ export default class RedisClusterSlots< #createClient(node: ShardNode, readonly = node.readonly) { return this.#clientFactory( this.#clientOptionsDefaults({ + clientSideCache: this.clientSideCache, + RESP: this.#options.RESP, socket: this.#getNodeAddress(node.address) ?? { host: node.host, port: node.port diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 8b37f9c1aa7..f7385629262 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -9,11 +9,10 @@ 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 { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache'; import { BasicCommandParser } from '../client/parser'; -import { parseArgs } from '../commands/generic-transformers'; -import SingleEntryCache from '../single-entry-cache'; - +import { ASKING_CMD } from '../commands/ASKING'; +import SingleEntryCache from '../single-entry-cache' interface ClusterCommander< M extends RedisModules, F extends RedisFunctions, @@ -67,6 +66,41 @@ export interface RedisClusterOptions< * Useful when the cluster is running on another network */ nodeAddressMap?: NodeAddressMap; + /** + * Client Side Caching configuration for the pool. + * + * Enables Redis Servers and Clients to work together to cache results from commands + * sent to a server. The server will notify the client when cached results are no longer valid. + * In pooled mode, the cache is shared across all clients in the pool. + * + * Note: Client Side Caching is only supported with RESP3. + * + * @example Anonymous cache configuration + * ``` + * const client = createCluster({ + * clientSideCache: { + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }, + * minimum: 5 + * }); + * ``` + * + * @example Using a controllable cache + * ``` + * const cache = new BasicPooledClientSideCache({ + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }); + * const client = createCluster({ + * clientSideCache: cache, + * minimum: 5 + * }); + * ``` + */ + clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig; } // remove once request & response policies are ready @@ -149,6 +183,7 @@ export default class RedisCluster< > extends EventEmitter { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return async function (this: ProxyCluster, ...args: Array) { const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); @@ -273,6 +308,10 @@ export default class RedisCluster< return this._self.#slots.slots; } + get clientSideCache() { + return this._self.#slots.clientSideCache; + } + /** * An array of the cluster masters. * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node. @@ -390,6 +429,27 @@ export default class RedisCluster< // return this._commandOptionsProxy('policies', policies); // } + #handleAsk( + fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise + ) { + return async (client: RedisClientType, options?: ClusterCommandOptions) => { + const chainId = Symbol("asking chain"); + const opts = options ? {...options} : {}; + opts.chainId = chainId; + + + + const ret = await Promise.all( + [ + client.sendCommand([ASKING_CMD], {chainId: chainId}), + fn(client, opts) + ] + ); + + return ret[1]; + }; + } + async #execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, @@ -399,14 +459,15 @@ export default class RedisCluster< const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; let client = await this.#slots.getClient(firstKey, isReadonly); let i = 0; - let myOpts = options; + + let myFn = fn; while (true) { try { - return await fn(client, myOpts); + return await myFn(client, options); } catch (err) { - // reset to passed in options, if changed by an ask request - myOpts = options; + myFn = fn; + // TODO: error class if (++i > maxCommandRedirections || !(err instanceof Error)) { throw err; @@ -425,13 +486,7 @@ export default class RedisCluster< } 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}`) } ); - + myFn = this.#handleAsk(fn); continue; } @@ -582,10 +637,12 @@ export default class RedisCluster< } close() { + this._self.#slots.clientSideCache?.onPoolClose(); return this._self.#slots.close(); } destroy() { + this._self.#slots.clientSideCache?.onPoolClose(); return this._self.#slots.destroy(); } diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts index 8c77fd89239..869dc60bec9 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -29,12 +29,7 @@ export function parseGeoSearchArguments( from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchOptions, - store?: RedisArgument ) { - if (store !== undefined) { - parser.pushKey(store); - } - parser.pushKey(key); if (typeof from === 'string' || from instanceof Buffer) { diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts index eb8e12abe6d..34c6e0988e2 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts @@ -17,7 +17,12 @@ export default { options?: GeoSearchStoreOptions ) { parser.push('GEOSEARCHSTORE'); - parseGeoSearchArguments(parser, source, from, by, options, destination); + + if (destination !== undefined) { + parser.pushKey(destination); + } + + parseGeoSearchArguments(parser, source, from, by, options); if (options?.STOREDIST) { parser.push('STOREDIST'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index cf9228c261f..035cf09020d 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -5,11 +5,59 @@ import { RESP_TYPES } from '../RESP/decoder'; import { WatchError } from "../errors"; import { RedisSentinelConfig, SentinelFramework } from "./test-util"; import { RedisSentinelEvent, RedisSentinelType, RedisSentinelClientType, RedisNode } from "./types"; +import RedisSentinel from "./index"; import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; +import { BasicPooledClientSideCache } from '../client/cache' +import { once } from 'node:events' const execAsync = promisify(exec); +describe('RedisSentinel', () => { + describe('initialization', () => { + describe('clientSideCache validation', () => { + const clientSideCacheConfig = { ttl: 0, maxEntries: 0 }; + const options = { + name: 'mymaster', + sentinelRootNodes: [ + { host: 'localhost', port: 26379 } + ] + }; + + it('should throw error when clientSideCache is enabled with RESP 2', () => { + assert.throws( + () => RedisSentinel.create({ + ...options, + clientSideCache: clientSideCacheConfig, + RESP: 2 as const, + }), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should throw error when clientSideCache is enabled with RESP undefined', () => { + assert.throws( + () => RedisSentinel.create({ + ...options, + clientSideCache: clientSideCacheConfig, + }), + new Error('Client Side Caching is only supported with RESP3') + ); + }); + + it('should not throw when clientSideCache is enabled with RESP 3', () => { + assert.doesNotThrow(() => + RedisSentinel.create({ + ...options, + clientSideCache: clientSideCacheConfig, + RESP: 3 as const, + }) + ); + }); + }); + }); +}); + [GLOBAL.SENTINEL.OPEN, GLOBAL.SENTINEL.PASSWORD].forEach(testOptions => { const passIndex = testOptions.serverArguments.indexOf('--requirepass')+1; let password: string | undefined = undefined; @@ -967,6 +1015,34 @@ describe.skip('legacy tests', () => { tracer.push("added node and waiting on added promise"); await nodeAddedPromise; }) + + it('with client side caching', async function() { + this.timeout(30000); + const csc = new BasicPooledClientSideCache(); + + sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5}); + await sentinel.connect(); + + await sentinel.set('x', 1); + await sentinel.get('x'); + await sentinel.get('x'); + await sentinel.get('x'); + await sentinel.get('x'); + + assert.equal(1, csc.cacheMisses()); + assert.equal(3, csc.cacheHits()); + + const invalidatePromise = once(csc, 'invalidate'); + await sentinel.set('x', 2); + await invalidatePromise; + await sentinel.get('x'); + await sentinel.get('x'); + await sentinel.get('x'); + await sentinel.get('x'); + + assert.equal(csc.cacheMisses(), 2); + assert.equal(csc.cacheHits(), 6); + }) }); }); diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 3bf94abd819..ec570e64bf2 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -16,6 +16,7 @@ import { RedisVariadicArgument } from '../commands/generic-transformers'; import { WaitQueue } from './wait-queue'; import { TcpNetConnectOpts } from 'node:net'; import { RedisTcpSocketOptions } from '../client/socket'; +import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache'; interface ClientInfo { id: number; @@ -301,6 +302,10 @@ export default class RedisSentinel< #masterClientCount = 0; #masterClientInfo?: ClientInfo; + get clientSideCache() { + return this._self.#internal.clientSideCache; + } + constructor(options: RedisSentinelOptions) { super(); @@ -617,7 +622,7 @@ class RedisSentinelInternal< readonly #name: string; readonly #nodeClientOptions: RedisClientOptions; - readonly #sentinelClientOptions: RedisClientOptions; + readonly #sentinelClientOptions: RedisClientOptions; readonly #scanInterval: number; readonly #passthroughClientErrorEvents: boolean; @@ -650,9 +655,22 @@ class RedisSentinelInternal< #trace: (msg: string) => unknown = () => { }; + #clientSideCache?: PooledClientSideCacheProvider; + get clientSideCache() { + return this.#clientSideCache; + } + + #validateOptions(options?: RedisSentinelOptions) { + if (options?.clientSideCache && options?.RESP !== 3) { + throw new Error('Client Side Caching is only supported with RESP3'); + } + } + constructor(options: RedisSentinelOptions) { super(); + this.#validateOptions(options); + this.#name = options.name; this.#sentinelRootNodes = Array.from(options.sentinelRootNodes); @@ -662,11 +680,21 @@ class RedisSentinelInternal< this.#scanInterval = options.scanInterval ?? 0; this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false; - this.#nodeClientOptions = options.nodeClientOptions ? Object.assign({} as RedisClientOptions, options.nodeClientOptions) : {}; + this.#nodeClientOptions = (options.nodeClientOptions ? {...options.nodeClientOptions} : {}) as RedisClientOptions; if (this.#nodeClientOptions.url !== undefined) { throw new Error("invalid nodeClientOptions for Sentinel"); } + if (options.clientSideCache) { + if (options.clientSideCache instanceof PooledClientSideCacheProvider) { + this.#clientSideCache = this.#nodeClientOptions.clientSideCache = options.clientSideCache; + } else { + const cscConfig = options.clientSideCache; + this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig); +// this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); + } + } + this.#sentinelClientOptions = options.sentinelClientOptions ? Object.assign({} as RedisClientOptions, options.sentinelClientOptions) : {}; this.#sentinelClientOptions.modules = RedisSentinelModule; @@ -904,6 +932,8 @@ class RedisSentinelInternal< this.#isReady = false; + this.#clientSideCache?.onPoolClose(); + if (this.#scanTimer) { clearInterval(this.#scanTimer); this.#scanTimer = undefined; @@ -952,6 +982,8 @@ class RedisSentinelInternal< this.#isReady = false; + this.#clientSideCache?.onPoolClose(); + if (this.#scanTimer) { clearInterval(this.#scanTimer); this.#scanTimer = undefined; diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 60696bc3437..86bc5b31786 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -188,18 +188,22 @@ export class SentinelFramework extends DockerBase { } const options: RedisSentinelOptions = { + ...opts, 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 (!options.nodeClientOptions) { + options.nodeClientOptions = {}; + } + options.nodeClientOptions.password = this.config.password; - if (opts) { - Object.assign(options, opts); + if (!options.sentinelClientOptions) { + options.sentinelClientOptions = {}; + } + options.sentinelClientOptions = {password: this.config.password}; } return RedisSentinel.create(options); diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index 28a5a91ddd3..e72f2eec2a0 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -4,6 +4,7 @@ import { CommandSignature, CommanderConfig, RedisFunctions, RedisModules, RedisS import COMMANDS from '../commands'; import RedisSentinel, { RedisSentinelClient } from '.'; import { RedisTcpSocketOptions } from '../client/socket'; +import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache'; export interface RedisNode { host: string; @@ -67,6 +68,41 @@ export interface RedisSentinelOptions< * When `false`, the sentinel object will wait for the first available client from the pool. */ reserveClient?: boolean; + /** + * Client Side Caching configuration for the pool. + * + * Enables Redis Servers and Clients to work together to cache results from commands + * sent to a server. The server will notify the client when cached results are no longer valid. + * In pooled mode, the cache is shared across all clients in the pool. + * + * Note: Client Side Caching is only supported with RESP3. + * + * @example Anonymous cache configuration + * ``` + * const client = createSentinel({ + * clientSideCache: { + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }, + * minimum: 5 + * }); + * ``` + * + * @example Using a controllable cache + * ``` + * const cache = new BasicPooledClientSideCache({ + * ttl: 0, + * maxEntries: 0, + * evictPolicy: "LRU" + * }); + * const client = createSentinel({ + * clientSideCache: cache, + * minimum: 5 + * }); + * ``` + */ + clientSideCache?: PooledClientSideCacheProvider | ClientSideCacheConfig; } export interface SentinelCommander< diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index 90b789ddca9..7e2404c2f7a 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -1,5 +1,5 @@ -import { BasicCommandParser } from '../client/parser'; import { ArrayReply, Command, RedisFunction, RedisScript, RespVersions, UnwrapReply } from '../RESP/types'; +import { BasicCommandParser } from '../client/parser'; import { RedisSocketOptions, RedisTcpSocketOptions } from '../client/socket'; import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; import { NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode } from './types'; diff --git a/packages/redis/README.md b/packages/redis/README.md index f0b2a34905d..d04a19b0d71 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -234,6 +234,23 @@ of sending a `QUIT` command to the server, the client can simply close the netwo ```typescript client.destroy(); ``` +### Client Side Caching + +Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid. + +```typescript +// Enable client side caching with RESP3 +const client = createClient({ + RESP: 3, + clientSideCache: { + ttl: 0, // Time-to-live (0 = no expiration) + maxEntries: 0, // Maximum entries (0 = unlimited) + evictPolicy: "LRU" // Eviction policy: "LRU" or "FIFO" + } +}); +``` + +See the [V5 documentation](../../docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 8ed85bf6e3e..d92c5c9e3d8 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -450,7 +450,7 @@ export default class TestUtils { await fn(pool); } finally { await pool.flushAll(); - pool.destroy(); + pool.close(); } }); } From d0a5c4c94513023b785687064f2c43bf6cad8ef2 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Tue, 20 May 2025 11:04:11 +0300 Subject: [PATCH 116/244] Fix sentinel csc tests (#2966) Co-authored-by: H. Temelski --- packages/client/lib/sentinel/index.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index 035cf09020d..a2e52b774bb 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -1029,8 +1029,8 @@ describe.skip('legacy tests', () => { await sentinel.get('x'); await sentinel.get('x'); - assert.equal(1, csc.cacheMisses()); - assert.equal(3, csc.cacheHits()); + assert.equal(1, csc.stats().missCount); + assert.equal(3, csc.stats().hitCount); const invalidatePromise = once(csc, 'invalidate'); await sentinel.set('x', 2); @@ -1040,8 +1040,8 @@ describe.skip('legacy tests', () => { await sentinel.get('x'); await sentinel.get('x'); - assert.equal(csc.cacheMisses(), 2); - assert.equal(csc.cacheHits(), 6); + assert.equal(csc.stats().missCount, 2); + assert.equal(csc.stats().hitCount, 6); }) }); }); From f3d1d3352e28a8fe6f535f9459c2278c153b0599 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 14:28:15 +0300 Subject: [PATCH 117/244] feat(client): expose socketTimeout option (#2965) The maximum duration (in milliseconds) that the socket can remain idle (i.e., with no data sent or received) before being automatically closed. Default reconnectionStrategy will ignore the new SocketTimeoutError, but users are allowed to have custom strategies wich handle those errors in different ways --- docs/client-configuration.md | 8 ++- packages/client/lib/client/socket.spec.ts | 69 +++++++++++++++++++++-- packages/client/lib/client/socket.ts | 24 +++++++- packages/client/lib/errors.ts | 6 ++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 0564794ac46..1c9ba51a11d 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -9,6 +9,7 @@ | 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.socketTimeout | | The maximum duration (in milliseconds) that the socket can remain idle (i.e., with no data sent or received) before being automatically closed | | 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 | @@ -40,7 +41,12 @@ By default the strategy uses exponential backoff, but it can be overwritten like ```javascript createClient({ socket: { - reconnectStrategy: retries => { + reconnectStrategy: (retries, cause) => { + // By default, do not reconnect on socket timeout. + if (cause instanceof SocketTimeoutError) { + return false; + } + // 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: diff --git a/packages/client/lib/client/socket.spec.ts b/packages/client/lib/client/socket.spec.ts index 20b238a3a38..5117cc4f49d 100644 --- a/packages/client/lib/client/socket.spec.ts +++ b/packages/client/lib/client/socket.spec.ts @@ -2,13 +2,12 @@ import { strict as assert } from 'node:assert'; import { spy } from 'sinon'; import { once } from 'node:events'; import RedisSocket, { RedisSocketOptions } from './socket'; +import testUtils, { GLOBAL } from '../test-utils'; +import { setTimeout } from 'timers/promises'; describe('Socket', () => { function createSocket(options: RedisSocketOptions): RedisSocket { - const socket = new RedisSocket( - () => Promise.resolve(), - options - ); + const socket = new RedisSocket(() => Promise.resolve(), options); socket.on('error', () => { // ignore errors @@ -84,4 +83,66 @@ describe('Socket', () => { assert.equal(socket.isOpen, false); }); }); + + describe('socketTimeout', () => { + const timeout = 50; + testUtils.testWithClient( + 'should timeout with positive socketTimeout values', + async client => { + let timedOut = false; + + assert.equal(client.isReady, true, 'client.isReady'); + assert.equal(client.isOpen, true, 'client.isOpen'); + + client.on('error', err => { + assert.equal( + err.message, + `Socket timeout timeout. Expecting data, but didn't receive any in ${timeout}ms.` + ); + + assert.equal(client.isReady, false, 'client.isReady'); + + // This is actually a bug with the onSocketError implementation, + // the client should be closed before the error is emitted + process.nextTick(() => { + assert.equal(client.isOpen, false, 'client.isOpen'); + }); + + timedOut = true; + }); + await setTimeout(timeout * 2); + if (!timedOut) assert.fail('Should have timed out by now'); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + socket: { + socketTimeout: timeout + } + } + } + ); + + testUtils.testWithClient( + 'should not timeout with undefined socketTimeout', + async client => { + + assert.equal(client.isReady, true, 'client.isReady'); + assert.equal(client.isOpen, true, 'client.isOpen'); + + client.on('error', err => { + assert.fail('Should not have timed out or errored in any way'); + }); + await setTimeout(100); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + socket: { + socketTimeout: undefined + } + } + } + ); + }); }); diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts index 603416cf9ed..58ccbe0b0c5 100644 --- a/packages/client/lib/client/socket.ts +++ b/packages/client/lib/client/socket.ts @@ -1,7 +1,7 @@ import { EventEmitter, once } from 'node:events'; import net from 'node:net'; import tls from 'node:tls'; -import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors'; +import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError, SocketTimeoutError } from '../errors'; import { setTimeout } from 'node:timers/promises'; import { RedisArgument } from '../RESP/types'; @@ -23,6 +23,10 @@ type RedisSocketOptionsCommon = { * 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; + /** + * The timeout (in milliseconds) after which the socket will be closed. `undefined` means no timeout. + */ + socketTimeout?: number; } type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit< @@ -55,6 +59,7 @@ export default class RedisSocket extends EventEmitter { readonly #connectTimeout; readonly #reconnectStrategy; readonly #socketFactory; + readonly #socketTimeout; #socket?: net.Socket | tls.TLSSocket; @@ -85,6 +90,7 @@ export default class RedisSocket extends EventEmitter { this.#connectTimeout = options?.connectTimeout ?? 5000; this.#reconnectStrategy = this.#createReconnectStrategy(options); this.#socketFactory = this.#createSocketFactory(options); + this.#socketTimeout = options?.socketTimeout; } #createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction { @@ -103,7 +109,7 @@ export default class RedisSocket extends EventEmitter { return retryIn; } catch (err) { this.emit('error', err); - return this.defaultReconnectStrategy(retries); + return this.defaultReconnectStrategy(retries, err); } }; } @@ -253,6 +259,13 @@ export default class RedisSocket extends EventEmitter { socket.removeListener('timeout', onTimeout); } + if (this.#socketTimeout) { + socket.once('timeout', () => { + socket.destroy(new SocketTimeoutError(this.#socketTimeout!)); + }); + socket.setTimeout(this.#socketTimeout); + } + socket .once('error', err => this.#onSocketError(err)) .once('close', hadError => { @@ -341,7 +354,12 @@ export default class RedisSocket extends EventEmitter { this.#socket?.unref(); } - defaultReconnectStrategy(retries: number) { + defaultReconnectStrategy(retries: number, cause: unknown) { + // By default, do not reconnect on socket timeout. + if (cause instanceof SocketTimeoutError) { + return false; + } + // 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: diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index 8af4c5e5bed..db37ec1a9ba 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -16,6 +16,12 @@ export class ConnectionTimeoutError extends Error { } } +export class SocketTimeoutError extends Error { + constructor(timeout: number) { + super(`Socket timeout timeout. Expecting data, but didn't receive any in ${timeout}ms.`); + } +} + export class ClientClosedError extends Error { constructor() { super('The client is closed'); From 4a5f879ec9763d5522d3f0d9cf985a8418b50315 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 15:15:09 +0300 Subject: [PATCH 118/244] fix(client): bring disableClientInfo option back (#2959) * fix(client): bring disableClientInfo option back It disappeared in v5 fixes #2958 --- packages/client/lib/client/index.ts | 131 ++++++++++++------ .../client/lib/commands/CLIENT_INFO.spec.ts | 86 ++++++++++++ packages/client/lib/commands/CLIENT_INFO.ts | 13 +- packages/client/lib/errors.ts | 6 +- packages/client/tsconfig.json | 7 +- tsconfig.base.json | 3 +- 6 files changed, 196 insertions(+), 50 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index c7f94fe680a..8d98aa8ed26 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -4,11 +4,11 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP 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 { ClientClosedError, ClientOfflineError, DisconnectsClientError, SimpleError, 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, TransformReply } from '../RESP/types'; +import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply, TransformReply, CommandArguments } from '../RESP/types'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import { RedisMultiQueuedCommand } from '../multi-command'; import HELLO, { HelloOptions } from '../commands/HELLO'; @@ -19,6 +19,7 @@ import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../comm import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache'; import { BasicCommandParser, CommandParser } from './parser'; import SingleEntryCache from '../single-entry-cache'; +import { version } from '../../package.json' export interface RedisClientOptions< M extends RedisModules = RedisModules, @@ -135,6 +136,14 @@ export interface RedisClientOptions< * ``` */ clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig; + /** + * 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; } type WithCommands< @@ -514,7 +523,28 @@ export default class RedisClient< }); } - async #handshake(selectedDB: number) { + async #handshake(chainId: symbol, asap: boolean) { + const promises = []; + const commandsWithErrorHandlers = await this.#getHandshakeCommands(); + + if (asap) commandsWithErrorHandlers.reverse() + + for (const { cmd, errorHandler } of commandsWithErrorHandlers) { + promises.push( + this.#queue + .addCommand(cmd, { + chainId, + asap + }) + .catch(errorHandler) + ); + } + return promises; + } + + async #getHandshakeCommands(): Promise< + Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }> + > { const commands = []; const cp = this.#options?.credentialsProvider; @@ -532,8 +562,8 @@ export default class RedisClient< } if (cp && cp.type === 'streaming-credentials-provider') { - - const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + const [credentials, disposable] = + await this.#subscribeForStreamingCredentials(cp); this.#credentialsSubscription = disposable; if (credentials.password) { @@ -548,59 +578,88 @@ export default class RedisClient< hello.SETNAME = this.#options.name; } - commands.push( - parseArgs(HELLO, this.#options.RESP, hello) - ); + commands.push({ cmd: parseArgs(HELLO, this.#options.RESP, hello) }); } else { - if (cp && cp.type === 'async-credentials-provider') { - const credentials = await cp.credentials(); if (credentials.username || credentials.password) { - commands.push( - parseArgs(COMMANDS.AUTH, { + commands.push({ + cmd: parseArgs(COMMANDS.AUTH, { username: credentials.username, password: credentials.password ?? '' }) - ); + }); } } if (cp && cp.type === 'streaming-credentials-provider') { - - const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + const [credentials, disposable] = + await this.#subscribeForStreamingCredentials(cp); this.#credentialsSubscription = disposable; if (credentials.username || credentials.password) { - commands.push( - parseArgs(COMMANDS.AUTH, { + commands.push({ + cmd: parseArgs(COMMANDS.AUTH, { username: credentials.username, password: credentials.password ?? '' }) - ); + }); } } if (this.#options?.name) { - commands.push( - parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name) - ); + commands.push({ + cmd: parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name) + }); } } - if (selectedDB !== 0) { - commands.push(['SELECT', this.#selectedDB.toString()]); + if (this.#selectedDB !== 0) { + commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] }); } if (this.#options?.readonly) { - commands.push( - parseArgs(COMMANDS.READONLY) - ); + commands.push({ cmd: parseArgs(COMMANDS.READONLY) }); + } + + if (!this.#options?.disableClientInfo) { + commands.push({ + cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version], + errorHandler: (err: Error) => { + // Only throw if not a SimpleError - unknown subcommand + // Client libraries are expected to ignore failures + // of type SimpleError - unknown subcommand, which are + // expected from older servers ( < v7 ) + if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { + throw err; + } + } + }); + + commands.push({ + cmd: [ + 'CLIENT', + 'SETINFO', + 'LIB-NAME', + this.#options?.clientInfoTag + ? `node-redis(${this.#options.clientInfoTag})` + : 'node-redis' + ], + errorHandler: (err: Error) => { + // Only throw if not a SimpleError - unknown subcommand + // Client libraries are expected to ignore failures + // of type SimpleError - unknown subcommand, which are + // expected from older servers ( < v7 ) + if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { + throw err; + } + } + }); } if (this.#clientSideCache) { - commands.push(this.#clientSideCache.trackingOn()); + commands.push({cmd: this.#clientSideCache.trackingOn()}); } return commands; @@ -629,15 +688,7 @@ export default class RedisClient< ); } - const commands = await this.#handshake(this.#selectedDB); - for (let i = commands.length - 1; i >= 0; --i) { - promises.push( - this.#queue.addCommand(commands[i], { - chainId, - asap: true - }) - ); - } + promises.push(...(await this.#handshake(chainId, true))); if (promises.length) { this.#write(); @@ -1221,13 +1272,7 @@ export default class RedisClient< selectedDB = this._self.#options?.database ?? 0; 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 - }) - ); - } + promises.push(...(await this._self.#handshake(chainId, false))); this._self.#scheduleWrite(); await Promise.all(promises); this._self.#selectedDB = selectedDB; diff --git a/packages/client/lib/commands/CLIENT_INFO.spec.ts b/packages/client/lib/commands/CLIENT_INFO.spec.ts index 50345a46ce3..96881e6c1aa 100644 --- a/packages/client/lib/commands/CLIENT_INFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import CLIENT_INFO from './CLIENT_INFO'; import testUtils, { GLOBAL } from '../test-utils'; import { parseArgs } from './generic-transformers'; +import { version } from '../../package.json'; describe('CLIENT INFO', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -48,4 +49,89 @@ describe('CLIENT INFO', () => { } } }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.clientInfo Redis < 7', async client => { + const reply = await client.clientInfo(); + if (!testUtils.isVersionGreaterThan([7])) { + assert.strictEqual(reply.libName, undefined, 'LibName should be undefined for Redis < 7'); + assert.strictEqual(reply.libVer, undefined, 'LibVer should be undefined for Redis < 7'); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 info disabled', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, ''); + assert.equal(reply.libVer, ''); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + disableClientInfo: true + } + }); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag set', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, 'node-redis(client1)'); + assert.equal(reply.libVer, version); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + clientInfoTag: 'client1' + } + }); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag unset', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, 'node-redis'); + assert.equal(reply.libVer, version); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info enabled', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, 'node-redis(client1)'); + assert.equal(reply.libVer, version); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 2, + clientInfoTag: 'client1' + } + }); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info disabled', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, ''); + assert.equal(reply.libVer, ''); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + disableClientInfo: true, + RESP: 2 + } + }); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info enabled', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, 'node-redis(client1)'); + assert.equal(reply.libVer, version); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + clientInfoTag: 'client1' + } + }); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info disabled', async client => { + const reply = await client.clientInfo(); + assert.equal(reply.libName, ''); + assert.equal(reply.libVer, ''); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + disableClientInfo: true, + RESP: 3 + } + }); + }); diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index 36dac175443..8908bdb2600 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -52,6 +52,14 @@ export interface ClientInfoReply { * available since 7.0 */ resp?: number; + /** + * available since 7.0 + */ + libName?: string; + /** + * available since 7.0 + */ + libVer?: string; } const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; @@ -67,7 +75,6 @@ export default { 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, @@ -89,7 +96,9 @@ export default { totMem: Number(map['tot-mem']), events: map.events, cmd: map.cmd, - user: map.user + user: map.user, + libName: map['lib-name'], + libVer: map['lib-ver'] }; if (map.laddr !== undefined) { diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index db37ec1a9ba..4f05f62ac4b 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -70,7 +70,11 @@ export class ErrorReply extends Error { } } -export class SimpleError extends ErrorReply {} +export class SimpleError extends ErrorReply { + isUnknownSubcommand(): boolean { + return this.message.toLowerCase().indexOf('err unknown subcommand') !== -1; + } +} export class BlobError extends ErrorReply {} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 8caa47300d4..b1f7b44d915 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", }, "include": [ "./index.ts", - "./lib/**/*.ts" + "./lib/**/*.ts", + "./package.json" ], "exclude": [ "./lib/test-utils.ts", @@ -18,6 +19,6 @@ "./lib" ], "entryPointStrategy": "expand", - "out": "../../documentation/client" + "out": "../../documentation/client", } } diff --git a/tsconfig.base.json b/tsconfig.base.json index bd2bcac0845..d4a631fc008 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "sourceMap": true, "declaration": true, "declarationMap": true, - "allowJs": true + "allowJs": true, + "resolveJsonModule": true } } From 1294b4b8e02d4b079f5556717312650ae172dac4 Mon Sep 17 00:00:00 2001 From: Gary Burgmann Date: Tue, 20 May 2025 22:36:40 +1000 Subject: [PATCH 119/244] issue/2956 - document disableClientInfo (#2957) * issue/2956 - document disableClientInfo * issue/2956 - remove accidental disableOfflineQueue bold --- docs/client-configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 1c9ba51a11d..57af626bf71 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -28,6 +28,7 @@ | legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.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) | +| disableClientInfo | `false` | Disables `CLIENT SETINFO LIB-NAME node-redis` and `CLIENT SETINFO LIB-VER X.X.X` commands | ## Reconnect Strategy From 00845570ac0b728d8a196af5af3f1b436d0f523b Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 15:53:24 +0300 Subject: [PATCH 120/244] Release client@5.1.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 34d60154083..dd41dc53e0b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 4f7f83478ba7d89a1131a3c49771608a24236eef Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 15:57:45 +0300 Subject: [PATCH 121/244] Updated the Bloom package to use client@5.1.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40d291d9201..bb0da682490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.1" + "@redis/client": "^5.1.0" } }, "packages/client": { "name": "@redis/client", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz", + "integrity": "sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.0.1", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 0772fcd3bb8..e7b226f17a6 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.1" + "@redis/client": "^5.1.0" }, "devDependencies": { "@redis/test-utils": "*" From 3d53e254d421adfdd9b3432adf4ceb6bb0195246 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 15:59:14 +0300 Subject: [PATCH 122/244] Release bloom@5.1.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 e7b226f17a6..3c8e37c5b74 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From e938bda0d9f4682362949bd68df3f6df9e9ce5a1 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:01:04 +0300 Subject: [PATCH 123/244] Updated the Entraid package to use client@5.1.0 --- package-lock.json | 16 ++++++++++++++-- packages/entraid/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb0da682490..0577b4fb49a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.1" + "@redis/client": "^5.1.0" } }, "packages/entraid/node_modules/@types/node": { @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.0.1.tgz", + "integrity": "sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.0.1" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz", diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 57af37d36cd..43caebcc5bd 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.1" + "@redis/client": "^5.1.0" }, "devDependencies": { "@types/express": "^4.17.21", From f2a3c1bf5cae6368e87e6b1bac773ecb5cde516a Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:01:52 +0300 Subject: [PATCH 124/244] Release entraid@5.1.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 43caebcc5bd..ba44351b3b9 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From a1b41e2dbcfdaee19851e25a43106380fc613c72 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:03:07 +0300 Subject: [PATCH 125/244] Updated the Json package to use client@5.1.0 --- package-lock.json | 4 ++-- packages/json/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0577b4fb49a..78feb73b2c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8615,7 +8615,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.1" + "@redis/client": "^5.1.0" } }, "packages/redis": { diff --git a/packages/json/package.json b/packages/json/package.json index 5c2dfc49a45..72cbe6cb2c3 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.1" + "@redis/client": "^5.1.0" }, "devDependencies": { "@redis/test-utils": "*" From a485936a9f88ff8363b9695788dc2457845186c2 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:03:59 +0300 Subject: [PATCH 126/244] Release json@5.1.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 72cbe6cb2c3..fd0b3c768ff 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 4bb23283c36f00419b4be8fdd8473e6de42be3c6 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:05:08 +0300 Subject: [PATCH 127/244] Updated the Search package to use client@5.1.0 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78feb73b2c4..86c53b24b0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8656,6 +8656,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.0.1.tgz", + "integrity": "sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.0.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.0.1", @@ -8667,7 +8679,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.1" + "@redis/client": "^5.1.0" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index ba1fa2a74be..9ec7398d90b 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.1" + "@redis/client": "^5.1.0" }, "devDependencies": { "@redis/test-utils": "*" From 2cc68647fe26b49789a86539763091e04f0e9736 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:06:45 +0300 Subject: [PATCH 128/244] Release search@5.1.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 9ec7398d90b..ac56b5ff5db 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 7fca460305ae71d4058ce71009cd70fa034ac330 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:07:45 +0300 Subject: [PATCH 129/244] Updated the Timeseries package to use client@5.1.0 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86c53b24b0b..5f0c957a86d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8668,9 +8668,21 @@ "@redis/client": "^5.0.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.0.1.tgz", + "integrity": "sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.0.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8757,7 +8769,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.1" + "@redis/client": "^5.1.0" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 1b436701d6b..2fcec56b70b 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.1" + "@redis/client": "^5.1.0" }, "devDependencies": { "@redis/test-utils": "*" From 8d34ee207e7a3f6426c9d54a16ec00e50eb99876 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:12:49 +0300 Subject: [PATCH 130/244] Release time-series@5.1.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 2fcec56b70b..466c53df220 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 78c6d603f24915ff39adaf485124532a0f4ffd93 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:17:43 +0300 Subject: [PATCH 131/244] Updated the Redis package to use client@5.1.0 --- package-lock.json | 60 ++++--------------------------------- packages/redis/package.json | 10 +++---- 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f0c957a86d..a9f846e7a88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8622,64 +8622,16 @@ "version": "5.0.1", "license": "MIT", "dependencies": { - "@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" + "@redis/bloom": "5.1.0", + "@redis/client": "5.1.0", + "@redis/json": "5.1.0", + "@redis/search": "5.1.0", + "@redis/time-series": "5.1.0" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.0.1.tgz", - "integrity": "sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.0.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz", - "integrity": "sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.0.1.tgz", - "integrity": "sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.0.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.0.1.tgz", - "integrity": "sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.0.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.1.0", @@ -8760,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/redis/package.json b/packages/redis/package.json index e7c9da2660b..30c0fba90e6 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@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" + "@redis/bloom": "5.1.0", + "@redis/client": "5.1.0", + "@redis/json": "5.1.0", + "@redis/search": "5.1.0", + "@redis/time-series": "5.1.0" }, "engines": { "node": ">= 18" From ab399a8a22636bd4f4434fbdefa868b0ee581ab6 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:20:35 +0300 Subject: [PATCH 132/244] Release redis@5.1.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 30c0fba90e6..b6c3f409a69 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.1", + "version": "5.1.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From b8acbce213c0953f7b5874a8d0d1738805864e04 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:22:20 +0300 Subject: [PATCH 133/244] udpate package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index a9f846e7a88..ab4cbe138bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8619,7 +8619,7 @@ } }, "packages/redis": { - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@redis/bloom": "5.1.0", From 9ea260f0f9a3aba90c0c62ade443c23d5c44ab31 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 21 May 2025 11:38:29 +0300 Subject: [PATCH 134/244] fix(handshake): ignore errors on client.setinfo (#2969) As per the documentation (https://redis.io/docs/latest/commands/client-setinfo): Client libraries are expected to pipeline this command after authentication on all connections and ignore failures since they could be connected to an older version that doesn't support them. Turns out different versions of redis server return different errors, so its better to catch all. fixes #2968 --- packages/client/lib/client/index.ts | 26 +++++++++----------------- packages/client/lib/errors.ts | 6 +----- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 8d98aa8ed26..a446ad8e755 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -4,7 +4,7 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; -import { ClientClosedError, ClientOfflineError, DisconnectsClientError, SimpleError, WatchError } from '../errors'; +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'; @@ -626,14 +626,10 @@ export default class RedisClient< if (!this.#options?.disableClientInfo) { commands.push({ cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); @@ -646,14 +642,10 @@ export default class RedisClient< ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis' ], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); } diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index 4f05f62ac4b..db37ec1a9ba 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -70,11 +70,7 @@ export class ErrorReply extends Error { } } -export class SimpleError extends ErrorReply { - isUnknownSubcommand(): boolean { - return this.message.toLowerCase().indexOf('err unknown subcommand') !== -1; - } -} +export class SimpleError extends ErrorReply {} export class BlobError extends ErrorReply {} From 27537b0ab79b3a0dae4db1f03b5bb6eefacb32fb Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 22 May 2025 10:35:16 +0300 Subject: [PATCH 135/244] fix(cluster): replace native private with _ (#2971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Private class fields (e.g., #execute) cause runtime errors when accessed from contexts where `this` is not the exact instance, triggering “Receiver must be an instance of class RedisCluster”. fixes #2967 --- packages/client/lib/cluster/index.ts | 96 ++++++++++++++-------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index f7385629262..c2c251810e3 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -188,7 +188,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._commandOptions, @@ -204,7 +204,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._self._commandOptions, @@ -222,7 +222,7 @@ export default class RedisCluster< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, fn.IS_READ_ONLY, this._self._commandOptions, @@ -240,7 +240,7 @@ export default class RedisCluster< parser.push(...prefix); script.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, script.IS_READ_ONLY, this._commandOptions, @@ -293,9 +293,9 @@ export default class RedisCluster< return RedisCluster.factory(options)(options); } - readonly #options: RedisClusterOptions; + readonly _options: RedisClusterOptions; - readonly #slots: RedisClusterSlots; + readonly _slots: RedisClusterSlots; private _self = this; private _commandOptions?: ClusterCommandOptions; @@ -305,11 +305,11 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get slots() { - return this._self.#slots.slots; + return this._self._slots.slots; } get clientSideCache() { - return this._self.#slots.clientSideCache; + return this._self._slots.clientSideCache; } /** @@ -317,7 +317,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node. */ get masters() { - return this._self.#slots.masters; + return this._self._slots.masters; } /** @@ -325,7 +325,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node. */ get replicas() { - return this._self.#slots.replicas; + return this._self._slots.replicas; } /** @@ -333,25 +333,25 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get nodeByAddress() { - return this._self.#slots.nodeByAddress; + return this._self._slots.nodeByAddress; } /** * The current pub/sub node. */ get pubSubNode() { - return this._self.#slots.pubSubNode; + return this._self._slots.pubSubNode; } get isOpen() { - return this._self.#slots.isOpen; + return this._self._slots.isOpen; } constructor(options: RedisClusterOptions) { super(); - this.#options = options; - this.#slots = new RedisClusterSlots(options, this.emit.bind(this)); + this._options = options; + this._slots = new RedisClusterSlots(options, this.emit.bind(this)); if (options?.commandOptions) { this._commandOptions = options.commandOptions; @@ -366,14 +366,14 @@ export default class RedisCluster< _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING >(overrides?: Partial>) { return new (Object.getPrototypeOf(this).constructor)({ - ...this._self.#options, + ...this._self._options, commandOptions: this._commandOptions, ...overrides }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>; } async connect() { - await this._self.#slots.connect(); + await this._self._slots.connect(); return this as unknown as RedisClusterType; } @@ -429,7 +429,7 @@ export default class RedisCluster< // return this._commandOptionsProxy('policies', policies); // } - #handleAsk( + _handleAsk( fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ) { return async (client: RedisClientType, options?: ClusterCommandOptions) => { @@ -450,14 +450,14 @@ export default class RedisCluster< }; } - async #execute( + async _execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, options: ClusterCommandOptions | undefined, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { - const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; - let client = await this.#slots.getClient(firstKey, isReadonly); + const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; + let client = await this._slots.getClient(firstKey, isReadonly); let i = 0; let myFn = fn; @@ -475,10 +475,10 @@ export default class RedisCluster< if (err.message.startsWith('ASK')) { const address = err.message.substring(err.message.lastIndexOf(' ') + 1); - let redirectTo = await this.#slots.getMasterByAddress(address); + let redirectTo = await this._slots.getMasterByAddress(address); if (!redirectTo) { - await this.#slots.rediscover(client); - redirectTo = await this.#slots.getMasterByAddress(address); + await this._slots.rediscover(client); + redirectTo = await this._slots.getMasterByAddress(address); } if (!redirectTo) { @@ -486,13 +486,13 @@ export default class RedisCluster< } client = redirectTo; - myFn = this.#handleAsk(fn); + myFn = this._handleAsk(fn); continue; } if (err.message.startsWith('MOVED')) { - await this.#slots.rediscover(client); - client = await this.#slots.getClient(firstKey, isReadonly); + await this._slots.rediscover(client); + client = await this._slots.getClient(firstKey, isReadonly); continue; } @@ -508,7 +508,7 @@ export default class RedisCluster< options?: ClusterCommandOptions, // defaultPolicies?: CommandPolicies ): Promise { - return this._self.#execute( + return this._self._execute( firstKey, isReadonly, options, @@ -520,11 +520,11 @@ export default class RedisCluster< 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); + 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); + const client = await this._self._slots.getClient(firstKey, isReadonly); return client._executePipeline(commands); }, routing, @@ -539,7 +539,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .SUBSCRIBE(channels, listener, bufferMode); } @@ -550,7 +550,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.UNSUBSCRIBE(channels, listener, bufferMode) ); } @@ -562,7 +562,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .PSUBSCRIBE(patterns, listener, bufferMode); } @@ -573,7 +573,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.PUNSUBSCRIBE(patterns, listener, bufferMode) ); } @@ -585,9 +585,9 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - const maxCommandRedirections = this._self.#options.maxCommandRedirections ?? 16, + const maxCommandRedirections = this._self._options.maxCommandRedirections ?? 16, firstChannel = Array.isArray(channels) ? channels[0] : channels; - let client = await this._self.#slots.getShardedPubSubClient(firstChannel); + let client = await this._self._slots.getShardedPubSubClient(firstChannel); for (let i = 0; ; i++) { try { return await client.SSUBSCRIBE(channels, listener, bufferMode); @@ -597,8 +597,8 @@ export default class RedisCluster< } if (err.message.startsWith('MOVED')) { - await this._self.#slots.rediscover(client); - client = await this._self.#slots.getShardedPubSubClient(firstChannel); + await this._self._slots.rediscover(client); + client = await this._self._slots.getShardedPubSubClient(firstChannel); continue; } @@ -614,7 +614,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeShardedUnsubscribeCommand( + return this._self._slots.executeShardedUnsubscribeCommand( Array.isArray(channels) ? channels[0] : channels, client => client.SUNSUBSCRIBE(channels, listener, bufferMode) ); @@ -626,28 +626,28 @@ export default class RedisCluster< * @deprecated Use `close` instead. */ quit() { - return this._self.#slots.quit(); + return this._self._slots.quit(); } /** * @deprecated Use `destroy` instead. */ disconnect() { - return this._self.#slots.disconnect(); + return this._self._slots.disconnect(); } close() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.close(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.close(); } destroy() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.destroy(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.destroy(); } nodeClient(node: ShardNode) { - return this._self.#slots.nodeClient(node); + return this._self._slots.nodeClient(node); } /** @@ -655,7 +655,7 @@ export default class RedisCluster< * Userful for running "forward" commands (like PUBLISH) on a random node. */ getRandomNode() { - return this._self.#slots.getRandomNode(); + return this._self._slots.getRandomNode(); } /** @@ -663,7 +663,7 @@ export default class RedisCluster< * Useful for running readonly commands on a slot. */ getSlotRandomNode(slot: number) { - return this._self.#slots.getSlotRandomNode(slot); + return this._self._slots.getSlotRandomNode(slot); } /** From 065eb5e9145db9152923a44b63a356c55f76cf3b Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 22 May 2025 10:36:52 +0300 Subject: [PATCH 136/244] fix(json): remove debug console.logs (#2970) --- packages/json/lib/commands/helpers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts index 26ff12f6834..99579ce81cb 100644 --- a/packages/json/lib/commands/helpers.ts +++ b/packages/json/lib/commands/helpers.ts @@ -2,7 +2,6 @@ import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformer import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - console.log('transformRedisJsonNullReply', json) return isNullReply(json) ? json : transformRedisJsonReply(json); } @@ -17,6 +16,5 @@ export function transformRedisJsonArgument(json: RedisJSON): string { export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { const res = JSON.parse((json as unknown as UnwrapReply).toString()); - console.log('transformRedisJsonReply', json, res) return res; } From f346bad64e87730bfbe9a38fb639dbd759353332 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Tue, 27 May 2025 14:21:22 +0300 Subject: [PATCH 137/244] Adapt legacy sentinel tests to use the new test utils (#2976) * modified legacy sentinel tests * Adapt legacy sentinel tests to use the new test utils * modify tmpdir creation * reduced sentinel config timeouts, removed unneeded comment --------- Co-authored-by: H. Temelski --- packages/client/lib/client/index.spec.ts | 4 +- packages/client/lib/sentinel/index.spec.ts | 67 ++-- packages/client/lib/sentinel/test-util.ts | 347 ++++++++------------- packages/client/lib/test-utils.ts | 2 +- packages/test-utils/lib/dockers.ts | 81 +++-- packages/test-utils/lib/index.ts | 71 ++++- 6 files changed, 264 insertions(+), 308 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index cc052dd5b51..4f752210dbe 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -89,8 +89,8 @@ describe('Client', () => { && 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(); + const resultCreds = await result.credentialsProvider?.credentials(); + const expectedCreds = await expected.credentialsProvider?.credentials(); assert.deepEqual(resultCreds, expectedCreds); } else { assert.fail('Credentials provider type mismatch'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a2e52b774bb..a9bdc8cc95c 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -197,7 +197,6 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { await sentinel.functionLoad( @@ -377,12 +376,9 @@ describe(`test with masterPoolSize 2`, () => { }, 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) { + // wait a bit to ensure that sentinels are seeing eachother + await setTimeout(2000) let checkedMaster = false; let checkedReplicas = false; while (!checkedMaster || !checkedReplicas) { @@ -430,7 +426,7 @@ async function steadyState(frame: SentinelFramework) { } } -describe.skip('legacy tests', () => { +describe('legacy tests', () => { const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined }; const frame = new SentinelFramework(config); let tracer = new Array(); @@ -439,42 +435,30 @@ describe.skip('legacy tests', () => { 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); - + 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(); await frame.getAllRunning(); await steadyState(frame); longestTestDelta = 0; @@ -522,6 +506,10 @@ describe.skip('legacy tests', () => { await sentinel.destroy(); sentinel = undefined; } + + stopMeasuringBlocking = true; + + await frame.cleanup(); }) it('use', async function () { @@ -863,7 +851,6 @@ describe.skip('legacy tests', () => { it('shutdown sentinel node', async function () { this.timeout(60000); - sentinel = frame.getSentinelClient(); sentinel.setTracer(tracer); sentinel.on("error", () => { }); @@ -1020,7 +1007,7 @@ describe.skip('legacy tests', () => { this.timeout(30000); const csc = new BasicPooledClientSideCache(); - sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5}); + sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3 as const}, RESP: 3 as const, clientSideCache: csc, masterPoolSize: 5}); await sentinel.connect(); await sentinel.set('x', 1); diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 86bc5b31786..60c1a59689a 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -4,12 +4,13 @@ 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 RedisClient, {RedisClientType} from '../client'; import RedisSentinel from '.'; import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; const execAsync = promisify(exec); import RedisSentinelModule from './module' - +import TestUtils from '@redis/test-utils'; +import { DEBUG_MODE_ARGS } from '../test-utils' interface ErrorWithCode extends Error { code: string; } @@ -125,7 +126,6 @@ export interface RedisSentinelConfig { sentinelServerArgument?: Array sentinelName: string; - sentinelQuorum?: number; password?: string; } @@ -151,6 +151,7 @@ export interface SentinelController { } export class SentinelFramework extends DockerBase { + #testUtils: TestUtils; #nodeList: Awaited> = []; /* port -> docker info/client */ #nodeMap: Map>>>; @@ -170,7 +171,11 @@ export class SentinelFramework extends DockerBase { super(); this.config = config; - + this.#testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' + }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); } @@ -190,7 +195,7 @@ export class SentinelFramework extends DockerBase { const options: RedisSentinelOptions = { ...opts, name: this.config.sentinelName, - sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.docker.port } }), + sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.port } }), passthroughClientErrorEvents: errors } @@ -218,11 +223,11 @@ export class SentinelFramework extends DockerBase { 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.#nodeList = await this.spawnRedisSentinelNodes(2); + this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value)); - this.#sentinelList = await this.spawnRedisSentinelSentinels(); - this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); + this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3) + this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value)); this.#spawned = true; } @@ -234,11 +239,8 @@ export class SentinelFramework extends DockerBase { return Promise.all( [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( - async ({ docker, client }) => { - if (client.isOpen) { - client.destroy(); - } - this.dockerRemove(docker.dockerId); + async ({ dockerId }) => { + this.dockerRemove(dockerId); } ) ).finally(async () => { @@ -248,112 +250,33 @@ export class SentinelFramework extends DockerBase { }); } - 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(); + protected async spawnRedisSentinelNodes(replicasCount: number) { + const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + + const replicas: Array = [] + for (let i = 0; i < replicasCount; i++) { + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + replicas.push(replica) - const promises: Array> = []; + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) - 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; - }) - ); + await client.connect(); + await client.replicaOf("127.0.0.1", master.port); + await client.close(); } 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 - }; + ...replicas + ] } - 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) - ] + protected async spawnRedisSentinelSentinels(masterPort: number, sentinels: number) { + return this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, masterPort, this.config.sentinelName, sentinels) } async getAllRunning() { @@ -384,90 +307,71 @@ export class SentinelFramework extends DockerBase { } 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); + const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1) + this.#sentinelList.push(nodes[0]); + this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]); } async addNode() { const masterPort = await this.getMasterPort(); - const newNode = await this.spawnRedisSentinelNodeDocker(); + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) - if (this.config.password !== undefined) { - await newNode.client.configSet({'masterauth': this.config.password}) - } - await newNode.client.replicaOf('127.0.0.1', masterPort); + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) + + await client.connect(); + await client.replicaOf("127.0.0.1", masterPort); + await client.close(); + - this.#nodeList.push(newNode); - this.#nodeMap.set(newNode.docker.port.toString(), newNode); + this.#nodeList.push(replica); + this.#nodeMap.set(replica.port.toString(), replica); } 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}`); - } + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const info = await client.sentinel.sentinelMaster(this.config.sentinelName); + await client.close() - if (!master.client.isOpen) { - throw new Error(`Sentinel's expected master node (${info.port}) is now down`); - } + const master = this.#nodeMap.get(info.port); + if (master === undefined) { + throw new Error(`couldn't find master node for ${info.port}`); + } - return info.port; + if (tracer) { + tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`); } - throw new Error("Couldn't get master"); + return info.port; } async getMasterPort(tracer?: Array): Promise { const data = await this.getMaster(tracer) - return this.#nodeMap.get(data!)!.docker.port; + return this.#nodeMap.get(data!)!.port; } getRandomNode() { - return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].docker.port.toString(); + return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].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(); + if (node.port != masterPort) { + return node.port.toString(); } } } @@ -479,11 +383,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - if (node.client.isOpen) { - node.client.destroy(); - } - - return await this.dockerStop(node.docker.dockerId); + return await this.dockerStop(node.dockerId); } async restartNode(id: string) { @@ -492,15 +392,7 @@ export class SentinelFramework extends DockerBase { 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(); - } + await this.dockerStart(node.dockerId); } async stopSentinel(id: string) { @@ -509,11 +401,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - if (sentinel.client.isOpen) { - sentinel.client.destroy(); - } - - return await this.dockerStop(sentinel.docker.dockerId); + return await this.dockerStop(sentinel.dockerId); } async restartSentinel(id: string) { @@ -522,16 +410,7 @@ export class SentinelFramework extends DockerBase { 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(); - } + await this.dockerStart(sentinel.dockerId); } getNodePort(id: string) { @@ -540,13 +419,13 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - return node.docker.port; + return node.port; } getAllNodesPort() { let ports: Array = []; for (const node of this.#nodeList) { - ports.push(node.docker.port); + ports.push(node.port); } return ports @@ -555,7 +434,7 @@ export class SentinelFramework extends DockerBase { getAllDockerIds() { let ids = new Map(); for (const node of this.#nodeList) { - ids.set(node.docker.dockerId, node.docker.port); + ids.set(node.dockerId, node.port); } return ids; @@ -567,43 +446,67 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - return sentinel.docker.port; + return sentinel.port; } getAllSentinelsPort() { let ports: Array = []; for (const sentinel of this.#sentinelList) { - ports.push(sentinel.docker.port); + ports.push(sentinel.port); } return ports } getSetinel(i: number): string { - return this.#sentinelList[i].docker.port.toString(); + return this.#sentinelList[i].port.toString(); } - sentinelSentinels() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); - } - } + async sentinelSentinels() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const sentinels = client.sentinel.sentinelSentinels(this.config.sentinelName) + await client.close() + + return sentinels } - sentinelMaster() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - } - } + async sentinelMaster() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const master = client.sentinel.sentinelMaster(this.config.sentinelName) + await client.close() + + return master } - sentinelReplicas() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); - } - } + async sentinelReplicas() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const replicas = client.sentinel.sentinelReplicas(this.config.sentinelName) + await client.close() + + return replicas } } \ No newline at end of file diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 63f43ba5e6a..19bbafc66ea 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -14,7 +14,7 @@ const utils = TestUtils.createFromConfig({ export default utils; -const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? +export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index 3814a80923b..47257964f6a 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -62,7 +62,7 @@ export interface RedisServerDocker { dockerId: string; } -async function spawnRedisServerDocker( +export async function spawnRedisServerDocker( options: RedisServerDockerOptions, serverArguments: Array): Promise { let port; if (options.mode == "sentinel") { @@ -374,35 +374,16 @@ export async function spawnRedisSentinel( 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); - })()); + sentinelPromises.push( + spawnSentinelNode( + dockerConfigs, + serverArguments, + master.port, + "mymaster", + path.join(tmpDir, i.toString()), + password, + ), + ) } const sentinelNodes = await Promise.all(sentinelPromises); @@ -424,3 +405,43 @@ after(() => { }) ); }); + + +export async function spawnSentinelNode( + dockerConfigs: RedisServerDockerOptions, + serverArguments: Array, + masterPort: number, + sentinelName: string, + tmpDir: string, + password?: string, +) { + const port = (await portIterator.next()).value; + + let sentinelConfig = `port ${port} +sentinel monitor ${sentinelName} 127.0.0.1 ${masterPort} 2 +sentinel down-after-milliseconds ${sentinelName} 500 +sentinel failover-timeout ${sentinelName} 1000 +`; + if (password !== undefined) { + sentinelConfig += `requirepass ${password}\n`; + sentinelConfig += `sentinel auth-pass ${sentinelName} ${password}\n`; + } + + const dir = fs.mkdtempSync(tmpDir); + 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, + ); +} \ No newline at end of file diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index d92c5c9e3d8..a41f970e0c8 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -19,10 +19,13 @@ import { RedisClusterType } from '@redis/client/index'; import { RedisNode } from '@redis/client/lib/sentinel/types' -import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; +import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions, RedisServerDocker, spawnSentinelNode, spawnRedisServerDocker } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; interface TestUtilsConfig { /** @@ -395,19 +398,19 @@ export default class TestUtils { 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'}`) + >( + 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 = {}, @@ -541,4 +544,46 @@ export default class TestUtils { this.testWithClient(`client.${title}`, fn, options.client); this.testWithCluster(`cluster.${title}`, fn, options.cluster); } + + + spawnRedisServer< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions + ): Promise { + return spawnRedisServerDocker(this.#DOCKER_IMAGE, options.serverArguments) + } + + async spawnRedisSentinels< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions, + masterPort: number, + sentinelName: string, + count: number + ): Promise> { + const sentinels: Array = []; + for (let i = 0; i < count; i++) { + const appPrefix = 'sentinel-config-dir'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); + + sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir)) + + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true }); + } + } + + return sentinels + } } From 708b22f366d2825f5b2b58c49a764373e6814bfa Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 28 May 2025 13:15:37 +0530 Subject: [PATCH 138/244] docs(readme): replace relative GitHub links with absolute URLs (#2980) --- packages/redis/README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/redis/README.md b/packages/redis/README.md index d04a19b0d71..ab6b4707e6f 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -45,13 +45,13 @@ npm install redis | 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 | +| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/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)! @@ -83,7 +83,7 @@ createClient({ ``` 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). +the [client configuration guide](https://github.com/redis/node-redis/blob/master/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 @@ -188,7 +188,7 @@ await pool.ping(); ### Pub/Sub -See the [Pub/Sub overview](../../docs/pub-sub.md). +See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md). ### Scan Iterator @@ -250,7 +250,7 @@ const client = createClient({ }); ``` -See the [V5 documentation](../../docs/v5.md#client-side-caching) for more details and advanced usage. +See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining @@ -274,11 +274,11 @@ await Promise.all([ ### Programmability -See the [Programmability overview](../../docs/programmability.md). +See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md). ### Clustering -Check out the [Clustering Guide](../../docs/clustering.md) when using Node Redis to connect to a Redis Cluster. +Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster. ### Events @@ -291,12 +291,12 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `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) | +| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | See [here](https://github.com/redis/node-redis/blob/master/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. +> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions @@ -313,13 +313,13 @@ Node Redis is supported with the following versions of Redis: ## Migration -- [From V3 to V4](../../docs/v3-to-v4.md) -- [From V4 to V5](../../docs/v4-to-v5.md) -- [V5](../../docs/v5.md) +- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md) +- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md) +- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md) ## Contributing -If you'd like to contribute, check out the [contributing guide](../../CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md). Thank you to all the people who already contributed to Node Redis! @@ -327,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis! ## License -This repository is licensed under the "MIT" license. See [LICENSE](../../LICENSE). +This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE). From 4b939a7bdbc5791e1b33212da1890994c36ff5e7 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:16:25 +0300 Subject: [PATCH 139/244] Release client@5.1.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 dd41dc53e0b..a6d44451a62 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From bd6f764d91ee46d833a21c0671e8bc79693fcfa3 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:19:30 +0300 Subject: [PATCH 140/244] Updated the Bloom package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab4cbe138bc..9ffa7854a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/client": { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", + "integrity": "sha512-FMD35y2KgCWTBLOfF0MhwDSaIVcu5mOUuTV9Kw3JOWHMgON3+ulht31cjTB/gph0BfD1vzUvCeROeRaf/d+35w==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.0", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 3c8e37c5b74..e642650c283 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.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 3b03ced4ea5e7e8c316a9d38e607c91aa044d664 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:20:11 +0300 Subject: [PATCH 141/244] Release bloom@5.1.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 e642650c283..547e5ee64ed 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 566b16eb53b8d362dd910228832bac1f01df931c Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:21:25 +0300 Subject: [PATCH 142/244] Updated the Entraid package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/entraid/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ffa7854a89..d08701f4df6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/entraid/node_modules/@types/node": { @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.0.tgz", + "integrity": "sha512-Gp5RWvVKbvItMU2sd848yhY/BnigToz8H4PYcvlBBSP5cQ3lVP1LMh5Kx2CYBNzCdDabVicwBKNvaoLBqPNqIg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", diff --git a/packages/entraid/package.json b/packages/entraid/package.json index ba44351b3b9..bd20b01ac7d 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.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@types/express": "^4.17.21", From e4d903ca0cbe17577a3a06f9f54ffddd993c0630 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:21:59 +0300 Subject: [PATCH 143/244] Release entraid@5.1.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 bd20b01ac7d..43f26d1e039 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From e68b5deb190f9ab841ca2f987b2e5f5483096188 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:23:12 +0300 Subject: [PATCH 144/244] Updated the Json package to use client@5.1.1 --- package-lock.json | 4 ++-- packages/json/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d08701f4df6..21eeebac222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8615,7 +8615,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/redis": { diff --git a/packages/json/package.json b/packages/json/package.json index fd0b3c768ff..b3fe4a27ee7 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.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 8c7b384e6b51e0b74d19267a0c94e6e68baca952 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:23:31 +0300 Subject: [PATCH 145/244] Release json@5.1.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 b3fe4a27ee7..3b473dfce0f 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From b2213d813254b842f3b0f424b74a416fedd6178f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:24:32 +0300 Subject: [PATCH 146/244] Updated the Search package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21eeebac222..c33dc30b282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8656,6 +8656,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.0.tgz", + "integrity": "sha512-laXZt1Rlimk3py5ZoABBnd4xn/8dWbLUWGvVS7avgMhdczS+eWtXpElilJbFpc+7ZpVlol4vaSGFuR8ThDcTFw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.0", @@ -8667,7 +8679,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index ac56b5ff5db..a44f6322a7f 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.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 13566cf9d6a85ce076d8bd4df735353bb71a7686 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:24:51 +0300 Subject: [PATCH 147/244] Release search@5.1.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 a44f6322a7f..7cb73dfc0a5 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From d178b7839a458bc7c3e20569a416e9d4c7a4d4cc Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:25:37 +0300 Subject: [PATCH 148/244] Updated the Timeseries package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c33dc30b282..325eefb08b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8668,9 +8668,21 @@ "@redis/client": "^5.1.0" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.0.tgz", + "integrity": "sha512-afQYMeIdWGNPjr84GxxgJVkolxMW3BBrlNOwhRHPdzbdGh0rTUPjOJpGHBig34ostHX6AhZ6QwqceU1zLluDEg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8757,7 +8769,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 466c53df220..3acef8b8668 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.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From d4380221c84a83734c2936ce3fc624c7b962b123 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:25:56 +0300 Subject: [PATCH 149/244] Release time-series@5.1.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 3acef8b8668..888f0f317e2 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From d374514309a4975367dfbf02d14538c721cd2f34 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:27:23 +0300 Subject: [PATCH 150/244] Updated the Redis package to use client@5.1.1 --- package-lock.json | 60 ++++--------------------------------- packages/redis/package.json | 10 +++---- 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 325eefb08b3..8fae6bef850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8622,64 +8622,16 @@ "version": "5.1.0", "license": "MIT", "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.0.tgz", - "integrity": "sha512-Gp5RWvVKbvItMU2sd848yhY/BnigToz8H4PYcvlBBSP5cQ3lVP1LMh5Kx2CYBNzCdDabVicwBKNvaoLBqPNqIg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", - "integrity": "sha512-FMD35y2KgCWTBLOfF0MhwDSaIVcu5mOUuTV9Kw3JOWHMgON3+ulht31cjTB/gph0BfD1vzUvCeROeRaf/d+35w==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.0.tgz", - "integrity": "sha512-laXZt1Rlimk3py5ZoABBnd4xn/8dWbLUWGvVS7avgMhdczS+eWtXpElilJbFpc+7ZpVlol4vaSGFuR8ThDcTFw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.0.tgz", - "integrity": "sha512-afQYMeIdWGNPjr84GxxgJVkolxMW3BBrlNOwhRHPdzbdGh0rTUPjOJpGHBig34ostHX6AhZ6QwqceU1zLluDEg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, "packages/search": { "name": "@redis/search", "version": "5.1.1", @@ -8760,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/redis/package.json b/packages/redis/package.json index b6c3f409a69..e9e863cc264 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" From e4a1ca467fcbc1942fa6eb2f7b60b081110544e4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:31:12 +0300 Subject: [PATCH 151/244] Release redis@5.1.1 --- package-lock.json | 2 +- packages/redis/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fae6bef850..6d2460ed19f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8619,7 +8619,7 @@ } }, "packages/redis": { - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@redis/bloom": "5.1.1", diff --git a/packages/redis/package.json b/packages/redis/package.json index e9e863cc264..05fb70cdd11 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.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 20c16e0c2c82c6637c84c3f280e86ddad7fe0849 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 3 Jun 2025 14:38:07 +0300 Subject: [PATCH 152/244] (docs) add jsdoc comments to command parsers (#2984) * (docs) bloom: add jsdocs for all commands * (docs) json: add jsdocs * (docs) search: add jsdocs for all commands * (docs) jsdocs for std commands * (docs) jsdoc comments to time series commands --- packages/bloom/lib/commands/bloom/ADD.ts | 6 +++ packages/bloom/lib/commands/bloom/CARD.ts | 5 +++ packages/bloom/lib/commands/bloom/EXISTS.ts | 6 +++ packages/bloom/lib/commands/bloom/INFO.ts | 5 +++ packages/bloom/lib/commands/bloom/INSERT.ts | 12 ++++++ .../bloom/lib/commands/bloom/LOADCHUNK.ts | 7 ++++ packages/bloom/lib/commands/bloom/MADD.ts | 6 +++ packages/bloom/lib/commands/bloom/MEXISTS.ts | 6 +++ packages/bloom/lib/commands/bloom/RESERVE.ts | 10 +++++ packages/bloom/lib/commands/bloom/SCANDUMP.ts | 6 +++ .../lib/commands/count-min-sketch/INCRBY.ts | 6 +++ .../lib/commands/count-min-sketch/INFO.ts | 5 +++ .../commands/count-min-sketch/INITBYDIM.ts | 7 ++++ .../commands/count-min-sketch/INITBYPROB.ts | 7 ++++ .../lib/commands/count-min-sketch/MERGE.ts | 6 +++ .../lib/commands/count-min-sketch/QUERY.ts | 6 +++ packages/bloom/lib/commands/cuckoo/ADD.ts | 6 +++ packages/bloom/lib/commands/cuckoo/ADDNX.ts | 6 +++ packages/bloom/lib/commands/cuckoo/COUNT.ts | 6 +++ packages/bloom/lib/commands/cuckoo/DEL.ts | 6 +++ packages/bloom/lib/commands/cuckoo/EXISTS.ts | 6 +++ packages/bloom/lib/commands/cuckoo/INFO.ts | 5 +++ packages/bloom/lib/commands/cuckoo/INSERT.ts | 9 +++++ .../bloom/lib/commands/cuckoo/INSERTNX.ts | 9 +++++ .../bloom/lib/commands/cuckoo/LOADCHUNK.ts | 7 ++++ packages/bloom/lib/commands/cuckoo/RESERVE.ts | 10 +++++ .../bloom/lib/commands/cuckoo/SCANDUMP.ts | 6 +++ packages/bloom/lib/commands/t-digest/ADD.ts | 6 +++ .../bloom/lib/commands/t-digest/BYRANK.ts | 6 +++ .../bloom/lib/commands/t-digest/BYREVRANK.ts | 6 +++ packages/bloom/lib/commands/t-digest/CDF.ts | 6 +++ .../bloom/lib/commands/t-digest/CREATE.ts | 7 ++++ packages/bloom/lib/commands/t-digest/INFO.ts | 5 +++ packages/bloom/lib/commands/t-digest/MAX.ts | 5 +++ packages/bloom/lib/commands/t-digest/MERGE.ts | 9 +++++ packages/bloom/lib/commands/t-digest/MIN.ts | 5 +++ .../bloom/lib/commands/t-digest/QUANTILE.ts | 6 +++ packages/bloom/lib/commands/t-digest/RANK.ts | 6 +++ packages/bloom/lib/commands/t-digest/RESET.ts | 5 +++ .../bloom/lib/commands/t-digest/REVRANK.ts | 6 +++ .../lib/commands/t-digest/TRIMMED_MEAN.ts | 7 ++++ 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 | 6 +++ packages/bloom/lib/commands/top-k/INFO.ts | 5 +++ packages/bloom/lib/commands/top-k/LIST.ts | 5 +++ .../lib/commands/top-k/LIST_WITHCOUNT.ts | 5 +++ packages/bloom/lib/commands/top-k/QUERY.ts | 6 +++ packages/bloom/lib/commands/top-k/RESERVE.ts | 10 +++++ packages/client/lib/commands/ACL_CAT.ts | 5 +++ packages/client/lib/commands/ACL_DELUSER.ts | 5 +++ packages/client/lib/commands/ACL_DRYRUN.ts | 6 +++ packages/client/lib/commands/ACL_GENPASS.ts | 5 +++ packages/client/lib/commands/ACL_GETUSER.ts | 5 +++ packages/client/lib/commands/ACL_LIST.ts | 4 ++ packages/client/lib/commands/ACL_LOAD.ts | 4 ++ packages/client/lib/commands/ACL_LOG.ts | 5 +++ packages/client/lib/commands/ACL_LOG_RESET.ts | 4 ++ packages/client/lib/commands/ACL_SAVE.ts | 4 ++ packages/client/lib/commands/ACL_SETUSER.ts | 6 +++ packages/client/lib/commands/ACL_USERS.ts | 4 ++ packages/client/lib/commands/ACL_WHOAMI.ts | 4 ++ packages/client/lib/commands/APPEND.ts | 6 +++ packages/client/lib/commands/ASKING.ts | 4 ++ packages/client/lib/commands/AUTH.ts | 7 ++++ packages/client/lib/commands/BGREWRITEAOF.ts | 4 ++ packages/client/lib/commands/BGSAVE.ts | 6 +++ packages/client/lib/commands/BITCOUNT.ts | 9 +++++ packages/client/lib/commands/BITFIELD.ts | 6 +++ packages/client/lib/commands/BITFIELD_RO.ts | 6 +++ packages/client/lib/commands/BITOP.ts | 7 ++++ packages/client/lib/commands/BITPOS.ts | 9 +++++ packages/client/lib/commands/BLMOVE.ts | 9 +++++ packages/client/lib/commands/BLMPOP.ts | 6 +++ packages/client/lib/commands/BLPOP.ts | 6 +++ packages/client/lib/commands/BRPOP.ts | 6 +++ packages/client/lib/commands/BRPOPLPUSH.ts | 7 ++++ packages/client/lib/commands/BZMPOP.ts | 6 +++ packages/client/lib/commands/BZPOPMAX.ts | 6 +++ packages/client/lib/commands/BZPOPMIN.ts | 6 +++ .../client/lib/commands/CLIENT_CACHING.ts | 5 +++ .../client/lib/commands/CLIENT_GETNAME.ts | 4 ++ .../client/lib/commands/CLIENT_GETREDIR.ts | 4 ++ packages/client/lib/commands/CLIENT_ID.ts | 4 ++ packages/client/lib/commands/CLIENT_INFO.ts | 4 ++ packages/client/lib/commands/CLIENT_KILL.ts | 5 +++ packages/client/lib/commands/CLIENT_LIST.ts | 5 +++ .../client/lib/commands/CLIENT_NO-EVICT.ts | 5 +++ .../client/lib/commands/CLIENT_NO-TOUCH.ts | 5 +++ packages/client/lib/commands/CLIENT_PAUSE.ts | 6 +++ .../client/lib/commands/CLIENT_SETNAME.ts | 5 +++ .../client/lib/commands/CLIENT_TRACKING.ts | 6 +++ .../lib/commands/CLIENT_TRACKINGINFO.ts | 4 ++ .../client/lib/commands/CLIENT_UNPAUSE.ts | 4 ++ .../client/lib/commands/CLUSTER_ADDSLOTS.ts | 5 +++ .../lib/commands/CLUSTER_ADDSLOTSRANGE.ts | 5 +++ .../client/lib/commands/CLUSTER_BUMPEPOCH.ts | 4 ++ .../commands/CLUSTER_COUNT-FAILURE-REPORTS.ts | 5 +++ .../lib/commands/CLUSTER_COUNTKEYSINSLOT.ts | 5 +++ .../client/lib/commands/CLUSTER_DELSLOTS.ts | 5 +++ .../lib/commands/CLUSTER_DELSLOTSRANGE.ts | 5 +++ .../client/lib/commands/CLUSTER_FAILOVER.ts | 5 +++ .../client/lib/commands/CLUSTER_FLUSHSLOTS.ts | 4 ++ .../client/lib/commands/CLUSTER_FORGET.ts | 5 +++ .../lib/commands/CLUSTER_GETKEYSINSLOT.ts | 6 +++ packages/client/lib/commands/CLUSTER_INFO.ts | 4 ++ .../client/lib/commands/CLUSTER_KEYSLOT.ts | 5 +++ packages/client/lib/commands/CLUSTER_LINKS.ts | 4 ++ packages/client/lib/commands/CLUSTER_MEET.ts | 6 +++ packages/client/lib/commands/CLUSTER_MYID.ts | 4 ++ .../client/lib/commands/CLUSTER_MYSHARDID.ts | 4 ++ packages/client/lib/commands/CLUSTER_NODES.ts | 4 ++ .../client/lib/commands/CLUSTER_REPLICAS.ts | 5 +++ .../client/lib/commands/CLUSTER_REPLICATE.ts | 5 +++ packages/client/lib/commands/CLUSTER_RESET.ts | 5 +++ .../client/lib/commands/CLUSTER_SAVECONFIG.ts | 4 ++ .../lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts | 5 +++ .../client/lib/commands/CLUSTER_SETSLOT.ts | 7 ++++ packages/client/lib/commands/CLUSTER_SLOTS.ts | 4 ++ packages/client/lib/commands/COMMAND.ts | 4 ++ packages/client/lib/commands/COMMAND_COUNT.ts | 4 ++ .../client/lib/commands/COMMAND_GETKEYS.ts | 5 +++ .../lib/commands/COMMAND_GETKEYSANDFLAGS.ts | 5 +++ packages/client/lib/commands/COMMAND_INFO.ts | 5 +++ packages/client/lib/commands/COMMAND_LIST.ts | 5 +++ packages/client/lib/commands/CONFIG_GET.ts | 5 +++ .../client/lib/commands/CONFIG_RESETSTAT.ts | 4 ++ .../client/lib/commands/CONFIG_REWRITE.ts | 4 ++ packages/client/lib/commands/CONFIG_SET.ts | 6 +++ packages/client/lib/commands/COPY.ts | 7 ++++ packages/client/lib/commands/DBSIZE.ts | 4 ++ packages/client/lib/commands/DECR.ts | 5 +++ packages/client/lib/commands/DECRBY.ts | 6 +++ packages/client/lib/commands/DEL.ts | 5 +++ packages/client/lib/commands/DISCARD.ts | 4 ++ packages/client/lib/commands/DUMP.ts | 5 +++ packages/client/lib/commands/ECHO.ts | 5 +++ packages/client/lib/commands/EVAL.ts | 6 +++ packages/client/lib/commands/EVALSHA.ts | 6 +++ packages/client/lib/commands/EVALSHA_RO.ts | 6 +++ packages/client/lib/commands/EVAL_RO.ts | 6 +++ packages/client/lib/commands/EXISTS.ts | 5 +++ packages/client/lib/commands/EXPIRE.ts | 7 ++++ packages/client/lib/commands/EXPIREAT.ts | 7 ++++ packages/client/lib/commands/EXPIRETIME.ts | 5 +++ packages/client/lib/commands/FAILOVER.ts | 5 +++ packages/client/lib/commands/FCALL.ts | 6 +++ packages/client/lib/commands/FCALL_RO.ts | 6 +++ packages/client/lib/commands/FLUSHALL.ts | 5 +++ packages/client/lib/commands/FLUSHDB.ts | 5 +++ .../client/lib/commands/FUNCTION_DELETE.ts | 5 +++ packages/client/lib/commands/FUNCTION_DUMP.ts | 4 ++ .../client/lib/commands/FUNCTION_FLUSH.ts | 5 +++ packages/client/lib/commands/FUNCTION_KILL.ts | 4 ++ packages/client/lib/commands/FUNCTION_LIST.ts | 5 +++ .../lib/commands/FUNCTION_LIST_WITHCODE.ts | 5 +++ packages/client/lib/commands/FUNCTION_LOAD.ts | 6 +++ .../client/lib/commands/FUNCTION_RESTORE.ts | 6 +++ .../client/lib/commands/FUNCTION_STATS.ts | 4 ++ packages/client/lib/commands/GEOADD.ts | 7 ++++ packages/client/lib/commands/GEODIST.ts | 8 ++++ packages/client/lib/commands/GEOHASH.ts | 6 +++ packages/client/lib/commands/GEOPOS.ts | 6 +++ packages/client/lib/commands/GEORADIUS.ts | 9 +++++ .../client/lib/commands/GEORADIUSBYMEMBER.ts | 9 +++++ .../lib/commands/GEORADIUSBYMEMBER_RO.ts | 9 +++++ .../lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts | 9 +++++ .../lib/commands/GEORADIUSBYMEMBER_STORE.ts | 10 +++++ .../lib/commands/GEORADIUSBYMEMBER_WITH.ts | 10 +++++ packages/client/lib/commands/GEORADIUS_RO.ts | 9 +++++ .../client/lib/commands/GEORADIUS_RO_WITH.ts | 10 +++++ .../client/lib/commands/GEORADIUS_STORE.ts | 10 +++++ .../client/lib/commands/GEORADIUS_WITH.ts | 10 +++++ packages/client/lib/commands/GEOSEARCH.ts | 8 ++++ .../client/lib/commands/GEOSEARCHSTORE.ts | 9 +++++ .../client/lib/commands/GEOSEARCH_WITH.ts | 9 +++++ packages/client/lib/commands/GET.ts | 5 +++ packages/client/lib/commands/GETBIT.ts | 6 +++ packages/client/lib/commands/GETDEL.ts | 5 +++ packages/client/lib/commands/GETEX.ts | 6 +++ packages/client/lib/commands/GETRANGE.ts | 7 ++++ packages/client/lib/commands/GETSET.ts | 6 +++ packages/client/lib/commands/HDEL.ts | 6 +++ packages/client/lib/commands/HELLO.ts | 6 +++ packages/client/lib/commands/HEXISTS.ts | 6 +++ packages/client/lib/commands/HEXPIRE.ts | 8 ++++ packages/client/lib/commands/HEXPIREAT.ts | 8 ++++ packages/client/lib/commands/HEXPIRETIME.ts | 6 +++ packages/client/lib/commands/HGET.ts | 6 +++ packages/client/lib/commands/HGETALL.ts | 5 +++ packages/client/lib/commands/HGETDEL.ts | 6 +++ packages/client/lib/commands/HGETEX.ts | 7 ++++ packages/client/lib/commands/HINCRBY.ts | 7 ++++ packages/client/lib/commands/HINCRBYFLOAT.ts | 7 ++++ packages/client/lib/commands/HKEYS.ts | 5 +++ packages/client/lib/commands/HLEN.ts | 5 +++ packages/client/lib/commands/HMGET.ts | 6 +++ packages/client/lib/commands/HPERSIST.ts | 6 +++ packages/client/lib/commands/HPEXPIRE.ts | 9 +++++ packages/client/lib/commands/HPEXPIREAT.ts | 9 +++++ packages/client/lib/commands/HPEXPIRETIME.ts | 8 ++++ packages/client/lib/commands/HPTTL.ts | 8 ++++ packages/client/lib/commands/HRANDFIELD.ts | 7 ++++ .../client/lib/commands/HRANDFIELD_COUNT.ts | 8 ++++ .../commands/HRANDFIELD_COUNT_WITHVALUES.ts | 8 ++++ packages/client/lib/commands/HSCAN.ts | 9 +++++ .../client/lib/commands/HSCAN_NOVALUES.ts | 6 +++ packages/client/lib/commands/HSET.ts | 9 +++++ packages/client/lib/commands/HSETEX.ts | 9 +++++ packages/client/lib/commands/HSETNX.ts | 9 +++++ packages/client/lib/commands/HSTRLEN.ts | 8 ++++ packages/client/lib/commands/HTTL.ts | 6 +++ packages/client/lib/commands/HVALS.ts | 5 +++ packages/client/lib/commands/INCR.ts | 7 ++++ packages/client/lib/commands/INCRBY.ts | 8 ++++ packages/client/lib/commands/INCRBYFLOAT.ts | 8 ++++ packages/client/lib/commands/INFO.ts | 7 ++++ packages/client/lib/commands/KEYS.ts | 7 ++++ packages/client/lib/commands/LASTSAVE.ts | 6 +++ .../client/lib/commands/LATENCY_DOCTOR.ts | 6 +++ packages/client/lib/commands/LATENCY_GRAPH.ts | 7 ++++ .../client/lib/commands/LATENCY_HISTORY.ts | 7 ++++ .../client/lib/commands/LATENCY_LATEST.ts | 6 +++ packages/client/lib/commands/LCS.ts | 8 ++++ packages/client/lib/commands/LCS_IDX.ts | 9 +++++ .../lib/commands/LCS_IDX_WITHMATCHLEN.ts | 6 +++ packages/client/lib/commands/LCS_LEN.ts | 6 +++ packages/client/lib/commands/LINDEX.ts | 8 ++++ packages/client/lib/commands/LINSERT.ts | 10 +++++ packages/client/lib/commands/LLEN.ts | 7 ++++ packages/client/lib/commands/LMOVE.ts | 10 +++++ packages/client/lib/commands/LMPOP.ts | 7 ++++ packages/client/lib/commands/LOLWUT.ts | 8 ++++ packages/client/lib/commands/LPOP.ts | 7 ++++ packages/client/lib/commands/LPOP_COUNT.ts | 8 ++++ packages/client/lib/commands/LPOS.ts | 9 +++++ packages/client/lib/commands/LPOS_COUNT.ts | 10 +++++ packages/client/lib/commands/LPUSH.ts | 8 ++++ packages/client/lib/commands/LPUSHX.ts | 8 ++++ packages/client/lib/commands/LRANGE.ts | 9 +++++ packages/client/lib/commands/LREM.ts | 9 +++++ packages/client/lib/commands/LSET.ts | 9 +++++ packages/client/lib/commands/LTRIM.ts | 9 +++++ packages/client/lib/commands/MEMORY_DOCTOR.ts | 6 +++ .../lib/commands/MEMORY_MALLOC-STATS.ts | 6 +++ packages/client/lib/commands/MEMORY_PURGE.ts | 6 +++ packages/client/lib/commands/MEMORY_STATS.ts | 6 +++ packages/client/lib/commands/MEMORY_USAGE.ts | 8 ++++ packages/client/lib/commands/MGET.ts | 7 ++++ packages/client/lib/commands/MIGRATE.ts | 12 ++++++ packages/client/lib/commands/MODULE_LIST.ts | 6 +++ packages/client/lib/commands/MODULE_LOAD.ts | 8 ++++ packages/client/lib/commands/MODULE_UNLOAD.ts | 7 ++++ packages/client/lib/commands/MOVE.ts | 8 ++++ packages/client/lib/commands/MSET.ts | 7 ++++ packages/client/lib/commands/MSETNX.ts | 7 ++++ .../client/lib/commands/OBJECT_ENCODING.ts | 7 ++++ packages/client/lib/commands/OBJECT_FREQ.ts | 7 ++++ .../client/lib/commands/OBJECT_IDLETIME.ts | 7 ++++ .../client/lib/commands/OBJECT_REFCOUNT.ts | 7 ++++ packages/client/lib/commands/PERSIST.ts | 7 ++++ packages/client/lib/commands/PEXPIRE.ts | 9 +++++ packages/client/lib/commands/PEXPIREAT.ts | 9 +++++ packages/client/lib/commands/PEXPIRETIME.ts | 7 ++++ packages/client/lib/commands/PFADD.ts | 8 ++++ packages/client/lib/commands/PFCOUNT.ts | 7 ++++ packages/client/lib/commands/PFMERGE.ts | 8 ++++ packages/client/lib/commands/PING.ts | 7 ++++ packages/client/lib/commands/PSETEX.ts | 9 +++++ packages/client/lib/commands/PTTL.ts | 7 ++++ packages/client/lib/commands/PUBLISH.ts | 8 ++++ .../client/lib/commands/PUBSUB_CHANNELS.ts | 7 ++++ packages/client/lib/commands/PUBSUB_NUMPAT.ts | 6 +++ packages/client/lib/commands/PUBSUB_NUMSUB.ts | 13 ++++++ .../lib/commands/PUBSUB_SHARDCHANNELS.ts | 7 ++++ .../client/lib/commands/PUBSUB_SHARDNUMSUB.ts | 13 ++++++ packages/client/lib/commands/RANDOMKEY.ts | 6 +++ packages/client/lib/commands/READONLY.ts | 6 +++ packages/client/lib/commands/READWRITE.ts | 6 +++ packages/client/lib/commands/RENAME.ts | 8 ++++ packages/client/lib/commands/RENAMENX.ts | 8 ++++ packages/client/lib/commands/REPLICAOF.ts | 8 ++++ .../client/lib/commands/RESTORE-ASKING.ts | 6 +++ packages/client/lib/commands/RESTORE.ts | 18 +++++++++ packages/client/lib/commands/ROLE.ts | 24 +++++++++++ packages/client/lib/commands/RPOP.ts | 7 ++++ packages/client/lib/commands/RPOPLPUSH.ts | 8 ++++ packages/client/lib/commands/RPOP_COUNT.ts | 8 ++++ packages/client/lib/commands/RPUSH.ts | 8 ++++ packages/client/lib/commands/RPUSHX.ts | 8 ++++ packages/client/lib/commands/SADD.ts | 8 ++++ packages/client/lib/commands/SAVE.ts | 6 +++ packages/client/lib/commands/SCAN.ts | 40 +++++++++++++++++++ packages/client/lib/commands/SCARD.ts | 7 ++++ packages/client/lib/commands/SCRIPT_DEBUG.ts | 7 ++++ packages/client/lib/commands/SCRIPT_EXISTS.ts | 7 ++++ packages/client/lib/commands/SCRIPT_FLUSH.ts | 7 ++++ packages/client/lib/commands/SCRIPT_KILL.ts | 6 +++ packages/client/lib/commands/SCRIPT_LOAD.ts | 7 ++++ packages/client/lib/commands/SDIFF.ts | 7 ++++ packages/client/lib/commands/SDIFFSTORE.ts | 8 ++++ packages/client/lib/commands/SET.ts | 9 +++++ packages/client/lib/commands/SETBIT.ts | 9 +++++ packages/client/lib/commands/SETEX.ts | 9 +++++ packages/client/lib/commands/SETNX.ts | 8 ++++ packages/client/lib/commands/SETRANGE.ts | 9 +++++ packages/client/lib/commands/SHUTDOWN.ts | 15 +++++++ packages/client/lib/commands/SINTER.ts | 7 ++++ packages/client/lib/commands/SINTERCARD.ts | 14 ++++++- packages/client/lib/commands/SINTERSTORE.ts | 8 ++++ packages/client/lib/commands/SISMEMBER.ts | 8 ++++ packages/client/lib/commands/SMEMBERS.ts | 7 ++++ packages/client/lib/commands/SMISMEMBER.ts | 8 ++++ packages/client/lib/commands/SMOVE.ts | 9 +++++ packages/client/lib/commands/SORT.ts | 24 +++++++++++ packages/client/lib/commands/SORT_RO.ts | 4 ++ packages/client/lib/commands/SORT_STORE.ts | 7 ++++ packages/client/lib/commands/SPOP.ts | 7 ++++ packages/client/lib/commands/SPOP_COUNT.ts | 8 ++++ packages/client/lib/commands/SPUBLISH.ts | 8 ++++ packages/client/lib/commands/SRANDMEMBER.ts | 7 ++++ .../client/lib/commands/SRANDMEMBER_COUNT.ts | 8 ++++ packages/client/lib/commands/SREM.ts | 9 +++++ packages/client/lib/commands/SSCAN.ts | 17 ++++++++ packages/client/lib/commands/STRLEN.ts | 8 ++++ packages/client/lib/commands/SUNION.ts | 8 ++++ packages/client/lib/commands/SUNIONSTORE.ts | 9 +++++ packages/client/lib/commands/SWAPDB.ts | 6 +++ packages/client/lib/commands/TIME.ts | 7 ++++ packages/client/lib/commands/TOUCH.ts | 8 ++++ packages/client/lib/commands/TTL.ts | 8 ++++ packages/client/lib/commands/TYPE.ts | 8 ++++ packages/client/lib/commands/UNLINK.ts | 8 ++++ packages/client/lib/commands/WAIT.ts | 9 +++++ packages/client/lib/commands/XACK.ts | 10 +++++ packages/client/lib/commands/XADD.ts | 30 ++++++++++++++ .../client/lib/commands/XADD_NOMKSTREAM.ts | 10 +++++ packages/client/lib/commands/XAUTOCLAIM.ts | 33 +++++++++++++++ .../client/lib/commands/XAUTOCLAIM_JUSTID.ts | 20 ++++++++++ packages/client/lib/commands/XCLAIM.ts | 30 ++++++++++++++ packages/client/lib/commands/XCLAIM_JUSTID.ts | 15 +++++++ packages/client/lib/commands/XDEL.ts | 12 ++++++ packages/client/lib/commands/XGROUP_CREATE.ts | 17 ++++++++ .../lib/commands/XGROUP_CREATECONSUMER.ts | 13 ++++++ .../client/lib/commands/XGROUP_DELCONSUMER.ts | 13 ++++++ .../client/lib/commands/XGROUP_DESTROY.ts | 12 ++++++ packages/client/lib/commands/XGROUP_SETID.ts | 16 ++++++++ .../client/lib/commands/XINFO_CONSUMERS.ts | 23 +++++++++++ packages/client/lib/commands/XINFO_GROUPS.ts | 23 +++++++++++ packages/client/lib/commands/XINFO_STREAM.ts | 31 ++++++++++++++ packages/client/lib/commands/XLEN.ts | 11 +++++ packages/client/lib/commands/XPENDING.ts | 23 +++++++++++ .../client/lib/commands/XPENDING_RANGE.ts | 33 +++++++++++++++ packages/client/lib/commands/XRANGE.ts | 30 ++++++++++++++ packages/client/lib/commands/XREAD.ts | 30 ++++++++++++++ packages/client/lib/commands/XREADGROUP.ts | 21 ++++++++++ packages/client/lib/commands/XREVRANGE.ts | 17 ++++++++ packages/client/lib/commands/XTRIM.ts | 20 ++++++++++ packages/client/lib/commands/ZADD.ts | 28 +++++++++++++ packages/client/lib/commands/ZADD_INCR.ts | 20 ++++++++++ packages/client/lib/commands/ZCARD.ts | 11 +++++ packages/client/lib/commands/ZCOUNT.ts | 7 ++++ packages/client/lib/commands/ZDIFF.ts | 5 +++ packages/client/lib/commands/ZDIFFSTORE.ts | 6 +++ .../client/lib/commands/ZDIFF_WITHSCORES.ts | 5 +++ packages/client/lib/commands/ZINCRBY.ts | 7 ++++ packages/client/lib/commands/ZINTER.ts | 6 +++ packages/client/lib/commands/ZINTERCARD.ts | 6 +++ packages/client/lib/commands/ZINTERSTORE.ts | 7 ++++ .../client/lib/commands/ZINTER_WITHSCORES.ts | 4 ++ packages/client/lib/commands/ZLEXCOUNT.ts | 7 ++++ packages/client/lib/commands/ZMPOP.ts | 7 ++++ packages/client/lib/commands/ZMSCORE.ts | 6 +++ packages/client/lib/commands/ZPOPMAX.ts | 5 +++ packages/client/lib/commands/ZPOPMAX_COUNT.ts | 6 +++ packages/client/lib/commands/ZPOPMIN.ts | 5 +++ packages/client/lib/commands/ZPOPMIN_COUNT.ts | 6 +++ packages/client/lib/commands/ZRANDMEMBER.ts | 5 +++ .../client/lib/commands/ZRANDMEMBER_COUNT.ts | 6 +++ .../commands/ZRANDMEMBER_COUNT_WITHSCORES.ts | 6 +++ packages/client/lib/commands/ZRANGE.ts | 8 ++++ packages/client/lib/commands/ZRANGEBYLEX.ts | 8 ++++ packages/client/lib/commands/ZRANGEBYSCORE.ts | 8 ++++ .../lib/commands/ZRANGEBYSCORE_WITHSCORES.ts | 4 ++ packages/client/lib/commands/ZRANGESTORE.ts | 9 +++++ .../client/lib/commands/ZRANGE_WITHSCORES.ts | 4 ++ packages/client/lib/commands/ZRANK.ts | 6 +++ .../client/lib/commands/ZRANK_WITHSCORE.ts | 4 ++ packages/client/lib/commands/ZREM.ts | 6 +++ .../client/lib/commands/ZREMRANGEBYLEX.ts | 7 ++++ .../client/lib/commands/ZREMRANGEBYRANK.ts | 7 ++++ .../client/lib/commands/ZREMRANGEBYSCORE.ts | 7 ++++ packages/client/lib/commands/ZREVRANK.ts | 6 +++ packages/client/lib/commands/ZSCAN.ts | 7 ++++ packages/client/lib/commands/ZSCORE.ts | 6 +++ packages/client/lib/commands/ZUNION.ts | 6 +++ packages/client/lib/commands/ZUNIONSTORE.ts | 7 ++++ .../client/lib/commands/ZUNION_WITHSCORES.ts | 4 ++ .../lib/sentinel/commands/SENTINEL_MASTER.ts | 5 +++ .../lib/sentinel/commands/SENTINEL_MONITOR.ts | 8 ++++ .../sentinel/commands/SENTINEL_REPLICAS.ts | 5 +++ .../sentinel/commands/SENTINEL_SENTINELS.ts | 5 +++ .../lib/sentinel/commands/SENTINEL_SET.ts | 6 +++ packages/json/lib/commands/ARRAPPEND.ts | 10 +++++ packages/json/lib/commands/ARRINDEX.ts | 12 ++++++ packages/json/lib/commands/ARRINSERT.ts | 11 +++++ packages/json/lib/commands/ARRLEN.ts | 9 +++++ packages/json/lib/commands/ARRPOP.ts | 10 +++++ packages/json/lib/commands/ARRTRIM.ts | 10 +++++ packages/json/lib/commands/CLEAR.ts | 9 +++++ packages/json/lib/commands/DEBUG_MEMORY.ts | 9 +++++ packages/json/lib/commands/DEL.ts | 9 +++++ packages/json/lib/commands/FORGET.ts | 9 +++++ packages/json/lib/commands/GET.ts | 9 +++++ packages/json/lib/commands/MERGE.ts | 9 +++++ packages/json/lib/commands/MGET.ts | 8 ++++ packages/json/lib/commands/MSET.ts | 10 +++++ packages/json/lib/commands/NUMINCRBY.ts | 9 +++++ packages/json/lib/commands/NUMMULTBY.ts | 9 +++++ packages/json/lib/commands/OBJKEYS.ts | 9 +++++ packages/json/lib/commands/OBJLEN.ts | 9 +++++ packages/json/lib/commands/RESP.ts | 8 ++++ packages/json/lib/commands/SET.ts | 13 ++++++ packages/json/lib/commands/STRAPPEND.ts | 10 +++++ packages/json/lib/commands/STRLEN.ts | 9 +++++ packages/json/lib/commands/TOGGLE.ts | 8 ++++ packages/json/lib/commands/TYPE.ts | 9 +++++ packages/search/lib/commands/AGGREGATE.ts | 12 ++++++ .../lib/commands/AGGREGATE_WITHCURSOR.ts | 10 +++++ packages/search/lib/commands/ALIASADD.ts | 6 +++ packages/search/lib/commands/ALIASDEL.ts | 5 +++ packages/search/lib/commands/ALIASUPDATE.ts | 6 +++ packages/search/lib/commands/ALTER.ts | 6 +++ packages/search/lib/commands/CONFIG_GET.ts | 5 +++ packages/search/lib/commands/CONFIG_SET.ts | 6 +++ packages/search/lib/commands/CREATE.ts | 16 ++++++++ packages/search/lib/commands/CURSOR_DEL.ts | 6 +++ packages/search/lib/commands/CURSOR_READ.ts | 8 ++++ packages/search/lib/commands/DICTADD.ts | 6 +++ packages/search/lib/commands/DICTDEL.ts | 6 +++ packages/search/lib/commands/DICTDUMP.ts | 5 +++ packages/search/lib/commands/DROPINDEX.ts | 7 ++++ packages/search/lib/commands/EXPLAIN.ts | 9 +++++ packages/search/lib/commands/EXPLAINCLI.ts | 8 ++++ packages/search/lib/commands/INFO.ts | 5 +++ .../search/lib/commands/PROFILE_AGGREGATE.ts | 9 +++++ .../search/lib/commands/PROFILE_SEARCH.ts | 9 +++++ packages/search/lib/commands/SEARCH.ts | 15 +++++++ .../search/lib/commands/SEARCH_NOCONTENT.ts | 8 ++++ packages/search/lib/commands/SPELLCHECK.ts | 10 +++++ packages/search/lib/commands/SUGADD.ts | 10 +++++ packages/search/lib/commands/SUGDEL.ts | 6 +++ packages/search/lib/commands/SUGGET.ts | 9 +++++ .../lib/commands/SUGGET_WITHPAYLOADS.ts | 8 ++++ .../search/lib/commands/SUGGET_WITHSCORES.ts | 8 ++++ .../SUGGET_WITHSCORES_WITHPAYLOADS.ts | 8 ++++ packages/search/lib/commands/SUGLEN.ts | 5 +++ packages/search/lib/commands/SYNDUMP.ts | 5 +++ packages/search/lib/commands/SYNUPDATE.ts | 9 +++++ packages/search/lib/commands/TAGVALS.ts | 6 +++ packages/search/lib/commands/_LIST.ts | 4 ++ packages/time-series/lib/commands/ADD.ts | 8 ++++ packages/time-series/lib/commands/ALTER.ts | 6 +++ packages/time-series/lib/commands/CREATE.ts | 6 +++ .../time-series/lib/commands/CREATERULE.ts | 9 +++++ packages/time-series/lib/commands/DECRBY.ts | 4 ++ packages/time-series/lib/commands/DEL.ts | 7 ++++ .../time-series/lib/commands/DELETERULE.ts | 6 +++ packages/time-series/lib/commands/GET.ts | 6 +++ packages/time-series/lib/commands/INCRBY.ts | 11 +++++ packages/time-series/lib/commands/INFO.ts | 5 +++ .../time-series/lib/commands/INFO_DEBUG.ts | 5 +++ packages/time-series/lib/commands/MADD.ts | 5 +++ packages/time-series/lib/commands/MGET.ts | 16 ++++++++ .../lib/commands/MGET_SELECTED_LABELS.ts | 7 ++++ .../lib/commands/MGET_WITHLABELS.ts | 6 +++ packages/time-series/lib/commands/MRANGE.ts | 12 ++++++ .../lib/commands/MRANGE_GROUPBY.ts | 22 ++++++++++ .../lib/commands/MRANGE_SELECTED_LABELS.ts | 13 ++++++ .../MRANGE_SELECTED_LABELS_GROUPBY.ts | 14 +++++++ .../lib/commands/MRANGE_WITHLABELS.ts | 12 ++++++ .../lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 9 +++++ .../time-series/lib/commands/MREVRANGE.ts | 8 ++++ .../lib/commands/MREVRANGE_GROUPBY.ts | 9 +++++ .../lib/commands/MREVRANGE_SELECTED_LABELS.ts | 9 +++++ .../MREVRANGE_SELECTED_LABELS_GROUPBY.ts | 10 +++++ .../lib/commands/MREVRANGE_WITHLABELS.ts | 8 ++++ .../commands/MREVRANGE_WITHLABELS_GROUPBY.ts | 9 +++++ .../time-series/lib/commands/QUERYINDEX.ts | 5 +++ packages/time-series/lib/commands/RANGE.ts | 4 ++ packages/time-series/lib/commands/REVRANGE.ts | 4 ++ 491 files changed, 3861 insertions(+), 1 deletion(-) diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts index e12d9cfa1d2..bf976606997 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Adds an item to a Bloom Filter + * @param parser - The command parser + * @param key - The name of the Bloom filter + * @param item - The item to add to the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('BF.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts index c2f9aeb00fc..e1873e1faf1 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -3,6 +3,11 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP export default { IS_READ_ONLY: true, + /** + * Returns the cardinality (number of items) in a Bloom Filter + * @param parser - The command parser + * @param key - The name of the Bloom filter to query + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('BF.CARD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts index b3f19af9516..db16253e2c7 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: true, + /** + * Checks if an item exists in a Bloom Filter + * @param parser - The command parser + * @param key - The name of the Bloom filter + * @param item - The item to check for existence + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('BF.EXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index 7074885a41e..bdf7d0fda9b 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -12,6 +12,11 @@ export type BfInfoReplyMap = TuplesToMapReply<[ export default { IS_READ_ONLY: true, + /** + * Returns information about a Bloom Filter, including capacity, size, number of filters, items inserted, and expansion rate + * @param parser - The command parser + * @param key - The name of the Bloom filter to get information about + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('BF.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts index b8dcef325f8..c607e015694 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -13,6 +13,18 @@ export interface BfInsertOptions { export default { IS_READ_ONLY: false, + /** + * Adds one or more items to a Bloom Filter, creating it if it does not exist + * @param parser - The command parser + * @param key - The name of the Bloom filter + * @param items - One or more items to add to the filter + * @param options - Optional parameters for filter creation + * @param options.CAPACITY - Desired capacity for a new filter + * @param options.ERROR - Desired error rate for a new filter + * @param options.EXPANSION - Expansion rate for a new filter + * @param options.NOCREATE - If true, prevents automatic filter creation + * @param options.NONSCALING - Prevents the filter from creating additional sub-filters + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts index ef3cc4a3e12..d0af9d7a644 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -3,6 +3,13 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Restores a Bloom Filter chunk previously saved using SCANDUMP + * @param parser - The command parser + * @param key - The name of the Bloom filter to restore + * @param iterator - Iterator value from the SCANDUMP command + * @param chunk - Data chunk from the SCANDUMP command + */ parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { parser.push('BF.LOADCHUNK'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts index efbd932b403..adef0aee3e2 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -5,6 +5,12 @@ import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/gene export default { IS_READ_ONLY: false, + /** + * Adds multiple items to a Bloom Filter in a single call + * @param parser - The command parser + * @param key - The name of the Bloom filter + * @param items - One or more items to add to the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('BF.MADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts index a5a311a8e4c..658f9b01229 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -5,6 +5,12 @@ import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/gene export default { IS_READ_ONLY: true, + /** + * Checks if multiple items exist in a Bloom Filter in a single call + * @param parser - The command parser + * @param key - The name of the Bloom filter + * @param items - One or more items to check for existence + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('BF.MEXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts index 00f17c1889f..d0b3fb906a9 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -8,6 +8,16 @@ export interface BfReserveOptions { export default { IS_READ_ONLY: true, + /** + * Creates an empty Bloom Filter with a given desired error ratio and initial capacity + * @param parser - The command parser + * @param key - The name of the Bloom filter to create + * @param errorRate - The desired probability for false positives (between 0 and 1) + * @param capacity - The number of entries intended to be added to the filter + * @param options - Optional parameters to tune the filter + * @param options.EXPANSION - Expansion rate for the filter + * @param options.NONSCALING - Prevents the filter from creating additional sub-filters + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts index d0472b649c5..4aebc5c9fc3 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -3,6 +3,12 @@ import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, export default { IS_READ_ONLY: true, + /** + * Begins an incremental save of a Bloom Filter. This is useful for large filters that can't be saved at once + * @param parser - The command parser + * @param key - The name of the Bloom filter to save + * @param iterator - Iterator value; Start at 0, and use the iterator from the response for the next chunk + */ parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { parser.push('BF.SCANDUMP'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts index a011957ece6..145047b207a 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts @@ -8,6 +8,12 @@ export interface BfIncrByItem { export default { IS_READ_ONLY: false, + /** + * Increases the count of one or more items in a Count-Min Sketch + * @param parser - The command parser + * @param key - The name of the sketch + * @param items - A single item or array of items to increment, each with an item and increment value + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts index fef1cac97e7..1f188bda013 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -16,6 +16,11 @@ export interface CmsInfoReply { export default { IS_READ_ONLY: true, + /** + * Returns width, depth, and total count of items in a Count-Min Sketch + * @param parser - The command parser + * @param key - The name of the sketch to get information about + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CMS.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts index 44e6a75952f..2bf9d97f208 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -3,6 +3,13 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Initialize a Count-Min Sketch using width and depth parameters + * @param parser - The command parser + * @param key - The name of the sketch + * @param width - Number of counters in each array (must be a multiple of 2) + * @param depth - Number of counter arrays (determines accuracy of estimates) + */ parseCommand(parser: CommandParser, key: RedisArgument, width: number, depth: number) { parser.push('CMS.INITBYDIM'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts index 3b96120bd04..180781d91e8 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -3,6 +3,13 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Initialize a Count-Min Sketch using error rate and probability parameters + * @param parser - The command parser + * @param key - The name of the sketch + * @param error - Estimate error, as a decimal between 0 and 1 + * @param probability - The desired probability for inflated count, as a decimal between 0 and 1 + */ parseCommand(parser: CommandParser, key: RedisArgument, error: number, probability: number) { parser.push('CMS.INITBYPROB'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts index 4d959bd619d..e4921c7975f 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -10,6 +10,12 @@ export type BfMergeSketches = Array | Array; export default { IS_READ_ONLY: false, + /** + * Merges multiple Count-Min Sketches into a single sketch, with optional weights + * @param parser - The command parser + * @param destination - The name of the destination sketch + * @param source - Array of sketch names or array of sketches with weights + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts index b55b51d1bbd..4322b0470c0 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: true, + /** + * Returns the count for one or more items in a Count-Min Sketch + * @param parser - The command parser + * @param key - The name of the sketch + * @param items - One or more items to get counts for + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('CMS.QUERY'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts index 37a5d1b5b86..db16acdea5a 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Adds an item to a Cuckoo Filter, creating the filter if it does not exist + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param item - The item to add to the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts index ceaf62be21c..ef6e1222e7f 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Adds an item to a Cuckoo Filter only if it does not exist + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param item - The item to add to the filter if it doesn't exist + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.ADDNX'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts index f0cd5a72105..e06d71f73e9 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -3,6 +3,12 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP export default { IS_READ_ONLY: true, + /** + * Returns the number of times an item appears in a Cuckoo Filter + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param item - The item to count occurrences of + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.COUNT'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts index c97b7c2d9fc..651a5bd8a8a 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Removes an item from a Cuckoo Filter if it exists + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param item - The item to remove from the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.DEL'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts index 2299cb3de99..820143ae886 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -4,6 +4,12 @@ import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Checks if an item exists in a Cuckoo Filter + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param item - The item to check for existence + */ parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.EXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts index 6a8f06f1e77..88622b5cb19 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -15,6 +15,11 @@ export type CfInfoReplyMap = TuplesToMapReply<[ export default { IS_READ_ONLY: true, + /** + * Returns detailed information about a Cuckoo Filter including size, buckets, filters count, items statistics and configuration + * @param parser - The command parser + * @param key - The name of the Cuckoo filter to get information about + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CF.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts index 3ad3feee16d..277c820cbcd 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -29,6 +29,15 @@ export function parseCfInsertArguments( export default { IS_READ_ONLY: false, + /** + * Adds one or more items to a Cuckoo Filter, creating it if it does not exist + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param items - One or more items to add to the filter + * @param options - Optional parameters for filter creation + * @param options.CAPACITY - The number of entries intended to be added to the filter + * @param options.NOCREATE - If true, prevents automatic filter creation + */ parseCommand(...args: Parameters) { args[0].push('CF.INSERT'); parseCfInsertArguments(...args); diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts index 7ddc952e2fa..bf99db6c3f7 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,6 +1,15 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import INSERT, { parseCfInsertArguments } from './INSERT'; +/** + * Adds one or more items to a Cuckoo Filter only if they do not exist yet, creating the filter if needed + * @param parser - The command parser + * @param key - The name of the Cuckoo filter + * @param items - One or more items to add to the filter + * @param options - Optional parameters for filter creation + * @param options.CAPACITY - The number of entries intended to be added to the filter + * @param options.NOCREATE - If true, prevents automatic filter creation + */ export default { IS_READ_ONLY: INSERT.IS_READ_ONLY, parseCommand(...args: Parameters) { diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts index 8fb21be8e0d..3a966e5145a 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -3,6 +3,13 @@ import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Restores a Cuckoo Filter chunk previously saved using SCANDUMP + * @param parser - The command parser + * @param key - The name of the Cuckoo filter to restore + * @param iterator - Iterator value from the SCANDUMP command + * @param chunk - Data chunk from the SCANDUMP command + */ parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { parser.push('CF.LOADCHUNK'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts index 2685b0db06d..26e31a1c645 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -9,6 +9,16 @@ export interface CfReserveOptions { export default { IS_READ_ONLY: false, + /** + * Creates an empty Cuckoo Filter with specified capacity and parameters + * @param parser - The command parser + * @param key - The name of the Cuckoo filter to create + * @param capacity - The number of entries intended to be added to the filter + * @param options - Optional parameters to tune the filter + * @param options.BUCKETSIZE - Number of items in each bucket + * @param options.MAXITERATIONS - Maximum number of iterations before declaring filter full + * @param options.EXPANSION - Number of additional buckets per expansion + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts index 25ef2c3f6da..96a036671f4 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -3,6 +3,12 @@ import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, Un export default { IS_READ_ONLY: true, + /** + * Begins an incremental save of a Cuckoo Filter. This is useful for large filters that can't be saved at once + * @param parser - The command parser + * @param key - The name of the Cuckoo filter to save + * @param iterator - Iterator value; Start at 0, and use the iterator from the response for the next chunk + */ parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { parser.push('CF.SCANDUMP'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts index 5534d58065b..30e745f8f47 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -3,6 +3,12 @@ import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Adds one or more observations to a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param values - Array of numeric values to add to the sketch + */ parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { parser.push('TDIGEST.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts index 9c1ab0059f3..9ac855bfeab 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -16,6 +16,12 @@ export function transformByRankArguments( export default { IS_READ_ONLY: true, + /** + * Returns value estimates for one or more ranks in a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param ranks - Array of ranks to get value estimates for (ascending order) + */ parseCommand(...args: Parameters) { args[0].push('TDIGEST.BYRANK'); transformByRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts index 8721c081e7a..a94e5566bb1 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -1,6 +1,12 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import BYRANK, { transformByRankArguments } from './BYRANK'; +/** + * Returns value estimates for one or more ranks in a t-digest sketch, starting from highest rank + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param ranks - Array of ranks to get value estimates for (descending order) + */ export default { IS_READ_ONLY: BYRANK.IS_READ_ONLY, parseCommand(...args: Parameters) { diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts index 4d1d8ea2786..a3d3b884a34 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -4,6 +4,12 @@ import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/gener export default { IS_READ_ONLY: true, + /** + * Estimates the cumulative distribution function for values in a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param values - Array of values to get CDF estimates for + */ parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { parser.push('TDIGEST.CDF'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts index 58b1e008284..25eb9c4f425 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -7,6 +7,13 @@ export interface TDigestCreateOptions { export default { IS_READ_ONLY: false, + /** + * Creates a new t-digest sketch for storing distributions + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param options - Optional parameters for sketch creation + * @param options.COMPRESSION - Compression parameter that affects performance and accuracy + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: TDigestCreateOptions) { parser.push('TDIGEST.CREATE'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts index 2cb9e93443c..96ae420886c 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -16,6 +16,11 @@ export type TdInfoReplyMap = TuplesToMapReply<[ export default { IS_READ_ONLY: true, + /** + * Returns information about a t-digest sketch including compression, capacity, nodes, weights, observations and memory usage + * @param parser - The command parser + * @param key - The name of the t-digest sketch to get information about + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts index 140db6a3e48..2353c60cdc6 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -4,6 +4,11 @@ import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-tr export default { IS_READ_ONLY: true, + /** + * Returns the maximum value from a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.MAX'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts index 80049d1e540..f34d30a7cef 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -9,6 +9,15 @@ export interface TDigestMergeOptions { export default { IS_READ_ONLY: false, + /** + * Merges multiple t-digest sketches into one, with optional compression and override settings + * @param parser - The command parser + * @param destination - The name of the destination t-digest sketch + * @param source - One or more source sketch names to merge from + * @param options - Optional parameters for merge operation + * @param options.COMPRESSION - New compression value for merged sketch + * @param options.OVERRIDE - If true, override destination sketch if it exists + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts index d6e56fb672e..9b8ea2a0cfc 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -4,6 +4,11 @@ import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-tr export default { IS_READ_ONLY: true, + /** + * Returns the minimum value from a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.MIN'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts index 1c27b5f6ec6..772172b84d5 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -4,6 +4,12 @@ import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/gener export default { IS_READ_ONLY: true, + /** + * Returns value estimates at requested quantiles from a t-digest sketch + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param quantiles - Array of quantiles (between 0 and 1) to get value estimates for + */ parseCommand(parser: CommandParser, key: RedisArgument, quantiles: Array) { parser.push('TDIGEST.QUANTILE'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts index 053c0c544e9..55dd3c636ce 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -15,6 +15,12 @@ export function transformRankArguments( export default { IS_READ_ONLY: true, + /** + * Returns the rank of one or more values in a t-digest sketch (number of values that are lower than each value) + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param values - Array of values to get ranks for + */ parseCommand(...args: Parameters) { args[0].push('TDIGEST.RANK'); transformRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts index c2bda72d6d4..8e1cefe9ff8 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -3,6 +3,11 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Resets a t-digest sketch, clearing all previously added observations + * @param parser - The command parser + * @param key - The name of the t-digest sketch to reset + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.RESET'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts index e7f2357a2f8..e323e10190a 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -1,6 +1,12 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import RANK, { transformRankArguments } from './RANK'; +/** + * Returns the reverse rank of one or more values in a t-digest sketch (number of values that are higher than each value) + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param values - Array of values to get reverse ranks for + */ export default { IS_READ_ONLY: RANK.IS_READ_ONLY, parseCommand(...args: Parameters) { diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts index 1fd6360ab65..f65f37a95de 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts @@ -4,6 +4,13 @@ import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-tr export default { IS_READ_ONLY: true, + /** + * Returns the mean value from a t-digest sketch after trimming values at specified percentiles + * @param parser - The command parser + * @param key - The name of the t-digest sketch + * @param lowCutPercentile - Lower percentile cutoff (between 0 and 100) + * @param highCutPercentile - Higher percentile cutoff (between 0 and 100) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts index 244e6209c91..8eab7dd5241 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: false, + /** + * Adds one or more items to a Top-K filter and returns items dropped from the top-K list + * @param parser - The command parser + * @param key - The name of the Top-K filter + * @param items - One or more items to add to the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts index 7e75a3b68aa..b72641471cd 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { IS_READ_ONLY: true, + /** + * Returns the count of occurrences for one or more items in a Top-K filter + * @param parser - The command parser + * @param key - The name of the Top-K filter + * @param items - One or more items to get counts for + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.COUNT'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts index 9e9a49e18f9..ef93653bd3c 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.ts @@ -12,6 +12,12 @@ function pushIncrByItem(parser: CommandParser, { item, incrementBy }: TopKIncrBy export default { IS_READ_ONLY: false, + /** + * Increases the score of one or more items in a Top-K filter by specified increments + * @param parser - The command parser + * @param key - The name of the Top-K filter + * @param items - A single item or array of items to increment, each with an item name and increment value + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts index 53da265c1f2..60ee3939324 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -12,6 +12,11 @@ export type TopKInfoReplyMap = TuplesToMapReply<[ export default { IS_READ_ONLY: true, + /** + * Returns configuration and statistics of a Top-K filter, including k, width, depth, and decay parameters + * @param parser - The command parser + * @param key - The name of the Top-K filter to get information about + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts index d7adeaa193c..e030ff02efa 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -3,6 +3,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/clie export default { IS_READ_ONLY: true, + /** + * Returns all items in a Top-K filter + * @param parser - The command parser + * @param key - The name of the Top-K filter + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.LIST'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts index 2c0f10e785b..bed58a36b70 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -3,6 +3,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, C export default { IS_READ_ONLY: true, + /** + * Returns all items in a Top-K filter with their respective counts + * @param parser - The command parser + * @param key - The name of the Top-K filter + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.LIST'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts index a6fb4bae69e..8976e211979 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client export default { IS_READ_ONLY: false, + /** + * Checks if one or more items are in the Top-K list + * @param parser - The command parser + * @param key - The name of the Top-K filter + * @param items - One or more items to check in the filter + */ parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.QUERY'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts index ee3ee9a8cf4..e31c2ceb99c 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.ts @@ -9,6 +9,16 @@ export interface TopKReserveOptions { export default { IS_READ_ONLY: false, + /** + * Creates a new Top-K filter with specified parameters + * @param parser - The command parser + * @param key - The name of the Top-K filter + * @param topK - Number of top occurring items to keep + * @param options - Optional parameters for filter configuration + * @param options.width - Number of counters in each array + * @param options.depth - Number of counter-arrays + * @param options.decay - Counter decay factor + */ parseCommand(parser: CommandParser, key: RedisArgument, topK: number, options?: TopKReserveOptions) { parser.push('TOPK.RESERVE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ACL_CAT.ts b/packages/client/lib/commands/ACL_CAT.ts index ae094b732b8..f4ddfacc68d 100644 --- a/packages/client/lib/commands/ACL_CAT.ts +++ b/packages/client/lib/commands/ACL_CAT.ts @@ -4,6 +4,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Lists ACL categories or commands in a category + * @param parser - The Redis command parser + * @param categoryName - Optional category name to filter commands + */ parseCommand(parser: CommandParser, categoryName?: RedisArgument) { parser.push('ACL', 'CAT'); if (categoryName) { diff --git a/packages/client/lib/commands/ACL_DELUSER.ts b/packages/client/lib/commands/ACL_DELUSER.ts index 5aa66becf75..404641e0abb 100644 --- a/packages/client/lib/commands/ACL_DELUSER.ts +++ b/packages/client/lib/commands/ACL_DELUSER.ts @@ -5,6 +5,11 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Deletes one or more users from the ACL + * @param parser - The Redis command parser + * @param username - Username(s) to delete + */ parseCommand(parser: CommandParser, username: RedisVariadicArgument) { parser.push('ACL', 'DELUSER'); parser.pushVariadic(username); diff --git a/packages/client/lib/commands/ACL_DRYRUN.ts b/packages/client/lib/commands/ACL_DRYRUN.ts index 09a51bc36f8..49ac41a859a 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.ts @@ -4,6 +4,12 @@ import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../R export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Simulates ACL operations without executing them + * @param parser - The Redis command parser + * @param username - Username to simulate ACL operations for + * @param command - Command arguments to simulate + */ parseCommand(parser: CommandParser, username: RedisArgument, command: Array) { parser.push('ACL', 'DRYRUN', username, ...command); }, diff --git a/packages/client/lib/commands/ACL_GENPASS.ts b/packages/client/lib/commands/ACL_GENPASS.ts index b5caa29b9b2..d1785839a5c 100644 --- a/packages/client/lib/commands/ACL_GENPASS.ts +++ b/packages/client/lib/commands/ACL_GENPASS.ts @@ -4,6 +4,11 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Generates a secure password for ACL users + * @param parser - The Redis command parser + * @param bits - Optional number of bits for password entropy + */ parseCommand(parser: CommandParser, bits?: number) { parser.push('ACL', 'GENPASS'); if (bits) { diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts index b4764ad744e..a1505251c6e 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -20,6 +20,11 @@ type AclUser = TuplesToMapReply<[ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns ACL information about a specific user + * @param parser - The Redis command parser + * @param username - Username to get information for + */ parseCommand(parser: CommandParser, username: RedisArgument) { parser.push('ACL', 'GETUSER', username); }, diff --git a/packages/client/lib/commands/ACL_LIST.ts b/packages/client/lib/commands/ACL_LIST.ts index b5f82cf272c..4d2ec995cd5 100644 --- a/packages/client/lib/commands/ACL_LIST.ts +++ b/packages/client/lib/commands/ACL_LIST.ts @@ -4,6 +4,10 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns all configured ACL users and their permissions + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('ACL', 'LIST'); }, diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts index dc4320b99fc..0367904a507 100644 --- a/packages/client/lib/commands/ACL_LOAD.ts +++ b/packages/client/lib/commands/ACL_LOAD.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Reloads ACL configuration from the ACL file + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('ACL', 'LOAD'); }, diff --git a/packages/client/lib/commands/ACL_LOG.ts b/packages/client/lib/commands/ACL_LOG.ts index 4cf2722ec86..a65f85039b1 100644 --- a/packages/client/lib/commands/ACL_LOG.ts +++ b/packages/client/lib/commands/ACL_LOG.ts @@ -21,6 +21,11 @@ export type AclLogReply = ArrayReply) { parser.push('CLIENT', 'KILL'); diff --git a/packages/client/lib/commands/CLIENT_LIST.ts b/packages/client/lib/commands/CLIENT_LIST.ts index 1e7f3d9ab40..1c1d9c3ec50 100644 --- a/packages/client/lib/commands/CLIENT_LIST.ts +++ b/packages/client/lib/commands/CLIENT_LIST.ts @@ -17,6 +17,11 @@ export type ListFilter = ListFilterType | ListFilterId; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information about all client connections. Can be filtered by type or ID + * @param parser - The Redis command parser + * @param filter - Optional filter to return only specific client types or IDs + */ parseCommand(parser: CommandParser, filter?: ListFilter) { parser.push('CLIENT', 'LIST'); if (filter) { diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.ts index de2f65270e2..116d226d036 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts @@ -4,6 +4,11 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Controls whether to prevent the client's connections from being evicted + * @param parser - The Redis command parser + * @param value - Whether to enable (true) or disable (false) the no-evict mode + */ parseCommand(parser: CommandParser, value: boolean) { parser.push( 'CLIENT', diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts index 8c6deff4af5..167b31f3600 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts @@ -4,6 +4,11 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Controls whether to prevent the client from touching the LRU/LFU of keys + * @param parser - The Redis command parser + * @param value - Whether to enable (true) or disable (false) the no-touch mode + */ parseCommand(parser: CommandParser, value: boolean) { parser.push( 'CLIENT', diff --git a/packages/client/lib/commands/CLIENT_PAUSE.ts b/packages/client/lib/commands/CLIENT_PAUSE.ts index ae6e4376364..4d0638df89d 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Stops the server from processing client commands for the specified duration + * @param parser - The Redis command parser + * @param timeout - Time in milliseconds to pause command processing + * @param mode - Optional mode: 'WRITE' to pause only write commands, 'ALL' to pause all commands + */ parseCommand(parser: CommandParser, timeout: number, mode?: 'WRITE' | 'ALL') { parser.push('CLIENT', 'PAUSE', timeout.toString()); if (mode) { diff --git a/packages/client/lib/commands/CLIENT_SETNAME.ts b/packages/client/lib/commands/CLIENT_SETNAME.ts index 335891e8308..7e4fcc17d94 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.ts @@ -4,6 +4,11 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Assigns a name to the current connection + * @param parser - The Redis command parser + * @param name - The name to assign to the connection + */ parseCommand(parser: CommandParser, name: RedisArgument) { parser.push('CLIENT', 'SETNAME', name); }, diff --git a/packages/client/lib/commands/CLIENT_TRACKING.ts b/packages/client/lib/commands/CLIENT_TRACKING.ts index df70a3705f9..08bed098c20 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.ts @@ -29,6 +29,12 @@ export type ClientTrackingOptions = CommonOptions & ( export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Controls server-assisted client side caching for the current connection + * @param parser - The Redis command parser + * @param mode - Whether to enable (true) or disable (false) tracking + * @param options - Optional configuration including REDIRECT, BCAST, PREFIX, OPTIN, OPTOUT, and NOLOOP options + */ parseCommand( parser: CommandParser, mode: M, diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts index fe6e090455c..1fb19e9e60b 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts @@ -10,6 +10,10 @@ type TrackingInfo = TuplesToMapReply<[ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information about the current connection's key tracking state + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLIENT', 'TRACKINGINFO'); }, diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.ts index c202e50a5df..2ac4fde0112 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Resumes processing of client commands after a CLIENT PAUSE + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLIENT', 'UNPAUSE'); }, diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts index 0f5c4513d1d..f833a42e5ad 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts @@ -4,6 +4,11 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Assigns hash slots to the current node in a Redis Cluster + * @param parser - The Redis command parser + * @param slots - One or more hash slots to be assigned + */ parseCommand(parser: CommandParser, slots: number | Array) { parser.push('CLUSTER', 'ADDSLOTS'); parser.pushVariadicNumber(slots); diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts index 40780731981..e3e7c8b4988 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts @@ -5,6 +5,11 @@ import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Assigns hash slot ranges to the current node in a Redis Cluster + * @param parser - The Redis command parser + * @param ranges - One or more slot ranges to be assigned, each specified as [start, end] + */ parseCommand(parser: CommandParser, ranges: SlotRange | Array) { parser.push('CLUSTER', 'ADDSLOTSRANGE'); parseSlotRangesArguments(parser, ranges); diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts index 04b62f85424..3cf9f3fb207 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Advances the cluster config epoch + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'BUMPEPOCH'); }, diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts index 0ac311f7ecd..b4421b1d8fa 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts @@ -4,6 +4,11 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the number of failure reports for a given node + * @param parser - The Redis command parser + * @param nodeId - The ID of the node to check + */ parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId); }, diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts index 63b4a8e02e2..df97c79161e 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts @@ -4,6 +4,11 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the number of keys in the specified hash slot + * @param parser - The Redis command parser + * @param slot - The hash slot to check + */ parseCommand(parser: CommandParser, slot: number) { parser.push('CLUSTER', 'COUNTKEYSINSLOT', slot.toString()); }, diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts index 9be6e962a18..eea7bcb699e 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts @@ -4,6 +4,11 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Removes hash slots from the current node in a Redis Cluster + * @param parser - The Redis command parser + * @param slots - One or more hash slots to be removed + */ parseCommand(parser: CommandParser, slots: number | Array) { parser.push('CLUSTER', 'DELSLOTS'); parser.pushVariadicNumber(slots); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts index 64c04021ba1..32b27d8ea14 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts @@ -5,6 +5,11 @@ import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Removes hash slot ranges from the current node in a Redis Cluster + * @param parser - The Redis command parser + * @param ranges - One or more slot ranges to be removed, each specified as [start, end] + */ parseCommand(parser:CommandParser, ranges: SlotRange | Array) { parser.push('CLUSTER', 'DELSLOTSRANGE'); parseSlotRangesArguments(parser, ranges); diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.ts index f74d65bd691..8a228c07349 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts @@ -15,6 +15,11 @@ export interface ClusterFailoverOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Forces a replica to perform a manual failover of its master + * @param parser - The Redis command parser + * @param options - Optional configuration with FORCE or TAKEOVER mode + */ parseCommand(parser:CommandParser, options?: ClusterFailoverOptions) { parser.push('CLUSTER', 'FAILOVER'); diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts index dab22b2e740..dac1f24c697 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Deletes all hash slots from the current node in a Redis Cluster + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'FLUSHSLOTS'); }, diff --git a/packages/client/lib/commands/CLUSTER_FORGET.ts b/packages/client/lib/commands/CLUSTER_FORGET.ts index 2928c3e9075..ff7cb952121 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.ts @@ -4,6 +4,11 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Removes a node from the cluster + * @param parser - The Redis command parser + * @param nodeId - The ID of the node to remove + */ parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'FORGET', nodeId); }, diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts index 2fd630ea1af..90756cf6302 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -4,6 +4,12 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns a number of keys from the specified hash slot + * @param parser - The Redis command parser + * @param slot - The hash slot to get keys from + * @param count - Maximum number of keys to return + */ parseCommand(parser: CommandParser, slot: number, count: number) { parser.push('CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()); }, diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts index 53140b38819..fa220b9f645 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -4,6 +4,10 @@ import { VerbatimStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information about the state of a Redis Cluster + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'INFO'); }, diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts index d81a14e1a96..1add4cb4f4a 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts @@ -4,6 +4,11 @@ import { Command, NumberReply, RedisArgument } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the hash slot number for a given key + * @param parser - The Redis command parser + * @param key - The key to get the hash slot for + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CLUSTER', 'KEYSLOT', key); }, diff --git a/packages/client/lib/commands/CLUSTER_LINKS.ts b/packages/client/lib/commands/CLUSTER_LINKS.ts index e98f61e762b..d35f4712650 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.ts @@ -13,6 +13,10 @@ type ClusterLinksReply = ArrayReply; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information about which Redis Cluster node handles which hash slots + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'SLOTS'); }, diff --git a/packages/client/lib/commands/COMMAND.ts b/packages/client/lib/commands/COMMAND.ts index 52eb7eb2fea..3d24b716a0d 100644 --- a/packages/client/lib/commands/COMMAND.ts +++ b/packages/client/lib/commands/COMMAND.ts @@ -5,6 +5,10 @@ import { CommandRawReply, CommandReply, transformCommandReply } from './generic- export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns an array with details about all Redis commands + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('COMMAND'); }, diff --git a/packages/client/lib/commands/COMMAND_COUNT.ts b/packages/client/lib/commands/COMMAND_COUNT.ts index ef561920b0b..36b35a58d7b 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.ts @@ -4,6 +4,10 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the total number of commands available in the Redis server + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('COMMAND', 'COUNT'); }, diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.ts b/packages/client/lib/commands/COMMAND_GETKEYS.ts index 97c5cb69ce2..6f447c4d4d6 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts @@ -4,6 +4,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Extracts the key names from a Redis command + * @param parser - The Redis command parser + * @param args - Command arguments to analyze + */ parseCommand(parser: CommandParser, args: Array) { parser.push('COMMAND', 'GETKEYS'); parser.push(...args); diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts index 72c1e16a2d1..1677adb43ec 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts @@ -9,6 +9,11 @@ export type CommandGetKeysAndFlagsRawReply = ArrayReply) { parser.push('COMMAND', 'GETKEYSANDFLAGS'); parser.push(...args); diff --git a/packages/client/lib/commands/COMMAND_INFO.ts b/packages/client/lib/commands/COMMAND_INFO.ts index fdf03780652..19629f0ccae 100644 --- a/packages/client/lib/commands/COMMAND_INFO.ts +++ b/packages/client/lib/commands/COMMAND_INFO.ts @@ -5,6 +5,11 @@ import { CommandRawReply, CommandReply, transformCommandReply } from './generic- export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns details about specific Redis commands + * @param parser - The Redis command parser + * @param commands - Array of command names to get information about + */ parseCommand(parser: CommandParser, commands: Array) { parser.push('COMMAND', 'INFO', ...commands); }, diff --git a/packages/client/lib/commands/COMMAND_LIST.ts b/packages/client/lib/commands/COMMAND_LIST.ts index ba518b70eca..fa218d86aa7 100644 --- a/packages/client/lib/commands/COMMAND_LIST.ts +++ b/packages/client/lib/commands/COMMAND_LIST.ts @@ -19,6 +19,11 @@ export interface CommandListOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns a list of all commands supported by the Redis server + * @param parser - The Redis command parser + * @param options - Options for filtering the command list + */ parseCommand(parser: CommandParser, options?: CommandListOptions) { parser.push('COMMAND', 'LIST'); diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index e8339c4d9a0..d0c80297fc4 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -5,6 +5,11 @@ import { RedisVariadicArgument, transformTuplesReply } from './generic-transform export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Gets the values of configuration parameters + * @param parser - The Redis command parser + * @param parameters - Pattern or specific configuration parameter names + */ parseCommand(parser: CommandParser, parameters: RedisVariadicArgument) { parser.push('CONFIG', 'GET'); parser.pushVariadic(parameters); diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts index 15de5ba7808..356a9b29a79 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Resets the statistics reported by Redis using the INFO command + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CONFIG', 'RESETSTAT'); }, diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts index ae6712ffb57..a9f2e0a41ba 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Rewrites the Redis configuration file with the current configuration + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('CONFIG', 'REWRITE'); }, diff --git a/packages/client/lib/commands/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts index dd1bbc29ef2..81b4c65c1d9 100644 --- a/packages/client/lib/commands/CONFIG_SET.ts +++ b/packages/client/lib/commands/CONFIG_SET.ts @@ -8,6 +8,12 @@ type MultipleParameters = [config: Record]; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Sets configuration parameters to the specified values + * @param parser - The Redis command parser + * @param parameterOrConfig - Either a single parameter name or a configuration object + * @param value - Value for the parameter (when using single parameter format) + */ parseCommand( parser: CommandParser, ...[parameterOrConfig, value]: SingleParameter | MultipleParameters diff --git a/packages/client/lib/commands/COPY.ts b/packages/client/lib/commands/COPY.ts index f26a930264c..0d8af5636df 100644 --- a/packages/client/lib/commands/COPY.ts +++ b/packages/client/lib/commands/COPY.ts @@ -8,6 +8,13 @@ export interface CopyCommandOptions { export default { IS_READ_ONLY: false, + /** + * Copies the value stored at the source key to the destination key + * @param parser - The Redis command parser + * @param source - Source key + * @param destination - Destination key + * @param options - Options for the copy operation + */ parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { parser.push('COPY'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts index 1ba1f060476..b5777b63f7d 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -4,6 +4,10 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the number of keys in the current database + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('DBSIZE'); }, diff --git a/packages/client/lib/commands/DECR.ts b/packages/client/lib/commands/DECR.ts index b9a6200d81b..5155fba81f5 100644 --- a/packages/client/lib/commands/DECR.ts +++ b/packages/client/lib/commands/DECR.ts @@ -2,6 +2,11 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Decrements the integer value of a key by one + * @param parser - The Redis command parser + * @param key - Key to decrement + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('DECR'); parser.pushKey(key); diff --git a/packages/client/lib/commands/DECRBY.ts b/packages/client/lib/commands/DECRBY.ts index 53a8315130d..9f35ee15a26 100644 --- a/packages/client/lib/commands/DECRBY.ts +++ b/packages/client/lib/commands/DECRBY.ts @@ -2,6 +2,12 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Decrements the integer value of a key by the given number + * @param parser - The Redis command parser + * @param key - Key to decrement + * @param decrement - Decrement amount + */ parseCommand(parser: CommandParser, key: RedisArgument, decrement: number) { parser.push('DECRBY'); parser.pushKey(key); diff --git a/packages/client/lib/commands/DEL.ts b/packages/client/lib/commands/DEL.ts index da0803f4d1b..7ad1b1160e7 100644 --- a/packages/client/lib/commands/DEL.ts +++ b/packages/client/lib/commands/DEL.ts @@ -4,6 +4,11 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes the specified keys. A key is ignored if it does not exist + * @param parser - The Redis command parser + * @param keys - One or more keys to delete + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('DEL'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/DISCARD.ts b/packages/client/lib/commands/DISCARD.ts index 1d30191c13d..d8c8c83791e 100644 --- a/packages/client/lib/commands/DISCARD.ts +++ b/packages/client/lib/commands/DISCARD.ts @@ -2,6 +2,10 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { + /** + * Discards a transaction, forgetting all queued commands + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('DISCARD'); }, diff --git a/packages/client/lib/commands/DUMP.ts b/packages/client/lib/commands/DUMP.ts index e442c1cdb2f..c4905cc71c4 100644 --- a/packages/client/lib/commands/DUMP.ts +++ b/packages/client/lib/commands/DUMP.ts @@ -3,6 +3,11 @@ import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Returns a serialized version of the value stored at the key + * @param parser - The Redis command parser + * @param key - Key to dump + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('DUMP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ECHO.ts b/packages/client/lib/commands/ECHO.ts index 7935bdc0101..b346ade50b1 100644 --- a/packages/client/lib/commands/ECHO.ts +++ b/packages/client/lib/commands/ECHO.ts @@ -4,6 +4,11 @@ import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the given string + * @param parser - The Redis command parser + * @param message - Message to echo back + */ parseCommand(parser: CommandParser, message: RedisArgument) { parser.push('ECHO', message); }, diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts index cdb8025b0be..ff244c82aaf 100644 --- a/packages/client/lib/commands/EVAL.ts +++ b/packages/client/lib/commands/EVAL.ts @@ -25,6 +25,12 @@ export function parseEvalArguments( export default { IS_READ_ONLY: false, + /** + * Executes a Lua script server side + * @param parser - The Redis command parser + * @param script - Lua script to execute + * @param options - Script execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('EVAL'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts index 5a9cc771358..29bb6ffdfcb 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -3,6 +3,12 @@ import EVAL, { parseEvalArguments } from './EVAL'; export default { IS_READ_ONLY: false, + /** + * Executes a Lua script server side using the script's SHA1 digest + * @param parser - The Redis command parser + * @param sha1 - SHA1 digest of the script + * @param options - Script execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('EVALSHA'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EVALSHA_RO.ts b/packages/client/lib/commands/EVALSHA_RO.ts index 24fadb3f486..628ca3dee53 100644 --- a/packages/client/lib/commands/EVALSHA_RO.ts +++ b/packages/client/lib/commands/EVALSHA_RO.ts @@ -3,6 +3,12 @@ import EVAL, { parseEvalArguments } from './EVAL'; export default { IS_READ_ONLY: true, + /** + * Executes a read-only Lua script server side using the script's SHA1 digest + * @param parser - The Redis command parser + * @param sha1 - SHA1 digest of the script + * @param options - Script execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('EVALSHA_RO'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EVAL_RO.ts b/packages/client/lib/commands/EVAL_RO.ts index 2438fd9d1dd..803c4f840c5 100644 --- a/packages/client/lib/commands/EVAL_RO.ts +++ b/packages/client/lib/commands/EVAL_RO.ts @@ -3,6 +3,12 @@ import EVAL, { parseEvalArguments } from './EVAL'; export default { IS_READ_ONLY: true, + /** + * Executes a read-only Lua script server side + * @param parser - The Redis command parser + * @param script - Lua script to execute + * @param options - Script execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('EVAL_RO'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EXISTS.ts b/packages/client/lib/commands/EXISTS.ts index 8ebb28269fe..ea6ea8cb0cd 100644 --- a/packages/client/lib/commands/EXISTS.ts +++ b/packages/client/lib/commands/EXISTS.ts @@ -5,6 +5,11 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Determines if the specified keys exist + * @param parser - The Redis command parser + * @param keys - One or more keys to check + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('EXISTS'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/EXPIRE.ts b/packages/client/lib/commands/EXPIRE.ts index 1e73b629492..15855832c32 100644 --- a/packages/client/lib/commands/EXPIRE.ts +++ b/packages/client/lib/commands/EXPIRE.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Sets a timeout on key. After the timeout has expired, the key will be automatically deleted + * @param parser - The Redis command parser + * @param key - Key to set expiration on + * @param seconds - Number of seconds until key expiration + * @param mode - Expiration mode: NX (only if key has no expiry), XX (only if key has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/EXPIREAT.ts b/packages/client/lib/commands/EXPIREAT.ts index 5bcb94ea321..4956b8aa23b 100644 --- a/packages/client/lib/commands/EXPIREAT.ts +++ b/packages/client/lib/commands/EXPIREAT.ts @@ -4,6 +4,13 @@ import { transformEXAT } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Sets the expiration for a key at a specific Unix timestamp + * @param parser - The Redis command parser + * @param key - Key to set expiration on + * @param timestamp - Unix timestamp (seconds since January 1, 1970) or Date object + * @param mode - Expiration mode: NX (only if key has no expiry), XX (only if key has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/EXPIRETIME.ts b/packages/client/lib/commands/EXPIRETIME.ts index 2bb97fb737b..faa8571eca3 100644 --- a/packages/client/lib/commands/EXPIRETIME.ts +++ b/packages/client/lib/commands/EXPIRETIME.ts @@ -3,6 +3,11 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key will expire + * @param parser - The Redis command parser + * @param key - Key to check expiration time + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('EXPIRETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/FAILOVER.ts b/packages/client/lib/commands/FAILOVER.ts index 1e98b983f96..24fa7a0347b 100644 --- a/packages/client/lib/commands/FAILOVER.ts +++ b/packages/client/lib/commands/FAILOVER.ts @@ -12,6 +12,11 @@ interface FailoverOptions { } export default { + /** + * Starts a coordinated failover between the primary and a replica + * @param parser - The Redis command parser + * @param options - Failover options including target host, abort flag, and timeout + */ parseCommand(parser: CommandParser, options?: FailoverOptions) { parser.push('FAILOVER'); diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts index 622060f693c..8fa56d4258e 100644 --- a/packages/client/lib/commands/FCALL.ts +++ b/packages/client/lib/commands/FCALL.ts @@ -3,6 +3,12 @@ import EVAL, { parseEvalArguments } from './EVAL'; export default { IS_READ_ONLY: false, + /** + * Invokes a Redis function + * @param parser - The Redis command parser + * @param functionName - Name of the function to call + * @param options - Function execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('FCALL'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts index 95effb0e698..5aac38aed0b 100644 --- a/packages/client/lib/commands/FCALL_RO.ts +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -3,6 +3,12 @@ import EVAL, { parseEvalArguments } from './EVAL'; export default { IS_READ_ONLY: false, + /** + * Invokes a read-only Redis function + * @param parser - The Redis command parser + * @param functionName - Name of the function to call + * @param options - Function execution options including keys and arguments + */ parseCommand(...args: Parameters) { args[0].push('FCALL_RO'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/FLUSHALL.ts b/packages/client/lib/commands/FLUSHALL.ts index c39535e8864..de6852d57e0 100644 --- a/packages/client/lib/commands/FLUSHALL.ts +++ b/packages/client/lib/commands/FLUSHALL.ts @@ -11,6 +11,11 @@ export type RedisFlushMode = typeof REDIS_FLUSH_MODES[keyof typeof REDIS_FLUSH_M export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Removes all keys from all databases + * @param parser - The Redis command parser + * @param mode - Optional flush mode (ASYNC or SYNC) + */ parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FLUSHALL'); if (mode) { diff --git a/packages/client/lib/commands/FLUSHDB.ts b/packages/client/lib/commands/FLUSHDB.ts index 5639f69a611..cd1ac201fce 100644 --- a/packages/client/lib/commands/FLUSHDB.ts +++ b/packages/client/lib/commands/FLUSHDB.ts @@ -5,6 +5,11 @@ import { RedisFlushMode } from './FLUSHALL'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Removes all keys from the current database + * @param parser - The Redis command parser + * @param mode - Optional flush mode (ASYNC or SYNC) + */ parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FLUSHDB'); if (mode) { diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts index dbfb044928e..e7b59ecb0cc 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -4,6 +4,11 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Deletes a library and all its functions + * @param parser - The Redis command parser + * @param library - Name of the library to delete + */ parseCommand(parser: CommandParser, library: RedisArgument) { parser.push('FUNCTION', 'DELETE', library); }, diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts index 2d0dbdd4455..73d6986b707 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -4,6 +4,10 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns a serialized payload representing the current functions loaded in the server + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'DUMP') }, diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts index 4ca59e4464e..8019fc0c215 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -5,6 +5,11 @@ import { RedisFlushMode } from './FLUSHALL'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Deletes all the libraries and functions from a Redis server + * @param parser - The Redis command parser + * @param mode - Optional flush mode (ASYNC or SYNC) + */ parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FUNCTION', 'FLUSH'); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts index 8b5351e93ab..b1626684b62 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -4,6 +4,10 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Kills a function that is currently executing + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'KILL'); }, diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts index 82e3697eadc..64ebaea8f85 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -20,6 +20,11 @@ export type FunctionListReply = ArrayReply) { FUNCTION_LIST.parseCommand(...args); args[0].push('WITHCODE'); diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts index 40b8ea8c0f4..0766a124afb 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -8,6 +8,12 @@ export interface FunctionLoadOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Loads a library to Redis + * @param parser - The Redis command parser + * @param code - Library code to load + * @param options - Function load options + */ parseCommand(parser: CommandParser, code: RedisArgument, options?: FunctionLoadOptions) { parser.push('FUNCTION', 'LOAD'); diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts index 944813f25e5..f18541a614a 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -8,6 +8,12 @@ export interface FunctionRestoreOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Restores libraries from the dump payload + * @param parser - The Redis command parser + * @param dump - Serialized payload of functions to restore + * @param options - Options for the restore operation + */ parseCommand(parser: CommandParser, dump: RedisArgument, options?: FunctionRestoreOptions) { parser.push('FUNCTION', 'RESTORE', dump); diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts index 908be5476e0..77eccf916bd 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -23,6 +23,10 @@ type FunctionStatsReply = TuplesToMapReply<[ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information about the function that is currently running and information about the available execution engines + * @param parser - The Redis command parser + */ parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'STATS'); }, diff --git a/packages/client/lib/commands/GEOADD.ts b/packages/client/lib/commands/GEOADD.ts index 31bf457e158..3da7b0e74b6 100644 --- a/packages/client/lib/commands/GEOADD.ts +++ b/packages/client/lib/commands/GEOADD.ts @@ -21,6 +21,13 @@ export interface GeoAddOptions { export default { IS_READ_ONLY: false, + /** + * Adds geospatial items to the specified key + * @param parser - The Redis command parser + * @param key - Key to add the geospatial items to + * @param toAdd - Geospatial member(s) to add + * @param options - Options for the GEOADD command + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEODIST.ts b/packages/client/lib/commands/GEODIST.ts index ba4d3080a71..f86d8156ebf 100644 --- a/packages/client/lib/commands/GEODIST.ts +++ b/packages/client/lib/commands/GEODIST.ts @@ -5,6 +5,14 @@ import { GeoUnits } from './GEOSEARCH'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the distance between two members in a geospatial index + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param member1 - First member in the geospatial index + * @param member2 - Second member in the geospatial index + * @param unit - Unit of distance (m, km, ft, mi) + */ parseCommand(parser: CommandParser, key: RedisArgument, member1: RedisArgument, diff --git a/packages/client/lib/commands/GEOHASH.ts b/packages/client/lib/commands/GEOHASH.ts index c3265d13157..bddc7a1fc0d 100644 --- a/packages/client/lib/commands/GEOHASH.ts +++ b/packages/client/lib/commands/GEOHASH.ts @@ -5,6 +5,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the Geohash string representation of one or more position members + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param member - One or more members in the geospatial index + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('GEOHASH'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts index 6bdbb65ac46..6fed2a8abc8 100644 --- a/packages/client/lib/commands/GEOPOS.ts +++ b/packages/client/lib/commands/GEOPOS.ts @@ -5,6 +5,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the longitude and latitude of one or more members in a geospatial index + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param member - One or more members in the geospatial index + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('GEOPOS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GEORADIUS.ts b/packages/client/lib/commands/GEORADIUS.ts index 5e8d880ab5e..2f622415064 100644 --- a/packages/client/lib/commands/GEORADIUS.ts +++ b/packages/client/lib/commands/GEORADIUS.ts @@ -18,6 +18,15 @@ export function parseGeoRadiusArguments( export default { IS_READ_ONLY: false, + /** + * Queries members in a geospatial index based on a radius from a center point + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center coordinates for the search + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param options - Additional search options + */ parseCommand(...args: Parameters) { args[0].push('GEORADIUS'); return parseGeoRadiusArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts index be4ca54650c..ee29ab84115 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts @@ -18,6 +18,15 @@ export function parseGeoRadiusByMemberArguments( export default { IS_READ_ONLY: false, + /** + * Queries members in a geospatial index based on a radius from a member + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Member name to use as center point + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param options - Additional search options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts index 335eea08133..8629694588a 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts @@ -4,6 +4,15 @@ import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSB export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Read-only variant that queries members in a geospatial index based on a radius from a member + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Member name to use as center point + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param options - Additional search options + */ parseCommand(...args: Parameters) { const parser = args[0]; parser.push('GEORADIUSBYMEMBER_RO'); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts index 06835438016..239c6cf2444 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts @@ -4,6 +4,15 @@ import GEORADIUSBYMEMBER_WITH, { parseGeoRadiusByMemberWithArguments } from './G export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Read-only variant that queries members in a geospatial index based on a radius from a member with additional information + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Member name to use as center point + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param withValues - Information to include with each returned member + */ parseCommand(...args: Parameters) { const parser = args[0]; parser.push('GEORADIUSBYMEMBER_RO'); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts index 676df34dd5a..20a1e0b6699 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts @@ -9,6 +9,16 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { export default { IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + /** + * Queries members in a geospatial index based on a radius from a member and stores the results + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Member name to use as center point + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param destination - Key to store the results + * @param options - Additional search and storage options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts index eefae0b27a9..9f7a01bb525 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts @@ -23,6 +23,16 @@ export function parseGeoRadiusByMemberWithArguments( export default { IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + /** + * Queries members in a geospatial index based on a radius from a member with additional information + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Member name to use as center point + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param replyWith - Information to include with each returned member + * @param options - Additional search options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUS_RO.ts b/packages/client/lib/commands/GEORADIUS_RO.ts index 5db65d9dc9b..29cf6f8ccd7 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.ts @@ -4,6 +4,15 @@ import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Read-only variant that queries members in a geospatial index based on a radius from a center point + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center coordinates for the search + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param options - Additional search options + */ parseCommand(...args: Parameters) { args[0].push('GEORADIUS_RO'); parseGeoRadiusArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts index cee1679382b..aaaef482f05 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts @@ -5,6 +5,16 @@ import GEORADIUS_WITH from './GEORADIUS_WITH'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Read-only variant that queries members in a geospatial index based on a radius from a center point with additional information + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center coordinates for the search + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param replyWith - Information to include with each returned member + * @param options - Additional search options + */ parseCommand(...args: Parameters) { args[0].push('GEORADIUS_RO'); parseGeoRadiusWithArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUS_STORE.ts b/packages/client/lib/commands/GEORADIUS_STORE.ts index 18459d44217..b2db8ca9882 100644 --- a/packages/client/lib/commands/GEORADIUS_STORE.ts +++ b/packages/client/lib/commands/GEORADIUS_STORE.ts @@ -9,6 +9,16 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { export default { IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + /** + * Queries members in a geospatial index based on a radius from a center point and stores the results + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center coordinates for the search + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param destination - Key to store the results + * @param options - Additional search and storage options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUS_WITH.ts b/packages/client/lib/commands/GEORADIUS_WITH.ts index ac4c8b7bb1b..5028a926145 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.ts @@ -20,6 +20,16 @@ export function parseGeoRadiusWithArguments( export default { IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + /** + * Queries members in a geospatial index based on a radius from a center point with additional information + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center coordinates for the search + * @param radius - Radius of the search area + * @param unit - Unit of distance (m, km, ft, mi) + * @param replyWith - Information to include with each returned member + * @param options - Additional search options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts index 869dc60bec9..a26ccec23eb 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -80,6 +80,14 @@ export function parseGeoSearchOptions( export default { IS_READ_ONLY: true, + /** + * Queries members inside an area of a geospatial index + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center point of the search (member name or coordinates) + * @param by - Search area specification (radius or box dimensions) + * @param options - Additional search options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts index 34c6e0988e2..194eafda818 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts @@ -8,6 +8,15 @@ export interface GeoSearchStoreOptions extends GeoSearchOptions { export default { IS_READ_ONLY: false, + /** + * Searches a geospatial index and stores the results in a new sorted set + * @param parser - The Redis command parser + * @param destination - Key to store the results + * @param source - Key of the geospatial index to search + * @param from - Center point of the search (member name or coordinates) + * @param by - Search area specification (radius or box dimensions) + * @param options - Additional search and storage options + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.ts b/packages/client/lib/commands/GEOSEARCH_WITH.ts index 65e3975b72f..dca125a816e 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.ts @@ -22,6 +22,15 @@ export interface GeoReplyWithMember { export default { IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY, + /** + * Queries members inside an area of a geospatial index with additional information + * @param parser - The Redis command parser + * @param key - Key of the geospatial index + * @param from - Center point of the search (member name or coordinates) + * @param by - Search area specification (radius or box dimensions) + * @param replyWith - Information to include with each returned member + * @param options - Additional search options + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GET.ts b/packages/client/lib/commands/GET.ts index ca013752ae5..e55c900eea2 100644 --- a/packages/client/lib/commands/GET.ts +++ b/packages/client/lib/commands/GET.ts @@ -4,6 +4,11 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets the value of a key + * @param parser - The Redis command parser + * @param key - Key to get the value of + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('GET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETBIT.ts b/packages/client/lib/commands/GETBIT.ts index 023ba0fb607..7d4a240473f 100644 --- a/packages/client/lib/commands/GETBIT.ts +++ b/packages/client/lib/commands/GETBIT.ts @@ -5,6 +5,12 @@ import { BitValue } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the bit value at a given offset in a string value + * @param parser - The Redis command parser + * @param key - Key to retrieve the bit from + * @param offset - Bit offset + */ parseCommand(parser: CommandParser, key: RedisArgument, offset: number) { parser.push('GETBIT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETDEL.ts b/packages/client/lib/commands/GETDEL.ts index a39014109f1..7dbdc7d2535 100644 --- a/packages/client/lib/commands/GETDEL.ts +++ b/packages/client/lib/commands/GETDEL.ts @@ -3,6 +3,11 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Gets the value of a key and deletes the key + * @param parser - The Redis command parser + * @param key - Key to get and delete + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('GETDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETEX.ts b/packages/client/lib/commands/GETEX.ts index e5ae0b691a7..836c2b5effe 100644 --- a/packages/client/lib/commands/GETEX.ts +++ b/packages/client/lib/commands/GETEX.ts @@ -39,6 +39,12 @@ export type GetExOptions = { export default { IS_READ_ONLY: true, + /** + * Gets the value of a key and optionally sets its expiration + * @param parser - The Redis command parser + * @param key - Key to get value from + * @param options - Options for setting expiration + */ parseCommand(parser: CommandParser, key: RedisArgument, options: GetExOptions) { parser.push('GETEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETRANGE.ts b/packages/client/lib/commands/GETRANGE.ts index ce0db6e3c03..f5f1586f0a9 100644 --- a/packages/client/lib/commands/GETRANGE.ts +++ b/packages/client/lib/commands/GETRANGE.ts @@ -4,6 +4,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns a substring of the string stored at a key + * @param parser - The Redis command parser + * @param key - Key to get substring from + * @param start - Start position of the substring + * @param end - End position of the substring + */ parseCommand(parser: CommandParser, key: RedisArgument, start: number, end: number) { parser.push('GETRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETSET.ts b/packages/client/lib/commands/GETSET.ts index 1b3312548e4..1b9d98f90d7 100644 --- a/packages/client/lib/commands/GETSET.ts +++ b/packages/client/lib/commands/GETSET.ts @@ -3,6 +3,12 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Sets a key to a new value and returns its old value + * @param parser - The Redis command parser + * @param key - Key to set + * @param value - Value to set + */ parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { parser.push('GETSET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HDEL.ts b/packages/client/lib/commands/HDEL.ts index 713d19a9b2a..cc5c4dab1b9 100644 --- a/packages/client/lib/commands/HDEL.ts +++ b/packages/client/lib/commands/HDEL.ts @@ -3,6 +3,12 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Removes one or more fields from a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param field - Field(s) to remove + */ parseCommand(parser: CommandParser, key: RedisArgument, field: RedisVariadicArgument) { parser.push('HDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HELLO.ts b/packages/client/lib/commands/HELLO.ts index 5d25998f987..23feaad554a 100644 --- a/packages/client/lib/commands/HELLO.ts +++ b/packages/client/lib/commands/HELLO.ts @@ -21,6 +21,12 @@ export type HelloReply = TuplesToMapReply<[ ]>; export default { + /** + * Handshakes with the Redis server and switches to the specified protocol version + * @param parser - The Redis command parser + * @param protover - Protocol version to use + * @param options - Additional options for authentication and connection naming + */ parseCommand(parser: CommandParser, protover?: RespVersions, options?: HelloOptions) { parser.push('HELLO'); diff --git a/packages/client/lib/commands/HEXISTS.ts b/packages/client/lib/commands/HEXISTS.ts index 9bb517b7df4..50b8f1ae8c2 100644 --- a/packages/client/lib/commands/HEXISTS.ts +++ b/packages/client/lib/commands/HEXISTS.ts @@ -4,6 +4,12 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Determines whether a field exists in a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param field - Field to check + */ parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { parser.push('HEXISTS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts index 55e2f5a9be1..95ee58eac1d 100644 --- a/packages/client/lib/commands/HEXPIRE.ts +++ b/packages/client/lib/commands/HEXPIRE.ts @@ -16,6 +16,14 @@ export const HASH_EXPIRATION = { export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION]; export default { + /** + * Sets a timeout on hash fields. After the timeout has expired, the fields will be automatically deleted + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param fields - Fields to set expiration on + * @param seconds - Number of seconds until field expiration + * @param mode - Expiration mode: NX (only if field has no expiry), XX (only if field has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts index 1370f2ecd65..c09efd4aa34 100644 --- a/packages/client/lib/commands/HEXPIREAT.ts +++ b/packages/client/lib/commands/HEXPIREAT.ts @@ -3,6 +3,14 @@ import { RedisVariadicArgument, transformEXAT } from './generic-transformers'; import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types'; export default { + /** + * Sets the expiration for hash fields at a specific Unix timestamp + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param fields - Fields to set expiration on + * @param timestamp - Unix timestamp (seconds since January 1, 1970) or Date object + * @param mode - Expiration mode: NX (only if field has no expiry), XX (only if field has existing expiry), GT (only if new expiry is greater than current), LT (only if new expiry is less than current) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts index 697d327db16..94504935090 100644 --- a/packages/client/lib/commands/HEXPIRETIME.ts +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -11,6 +11,12 @@ export const HASH_EXPIRATION_TIME = { export default { IS_READ_ONLY: true, + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given hash fields will expire + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param fields - Fields to check expiration time + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HGET.ts b/packages/client/lib/commands/HGET.ts index fcd9334eb0a..8c4d690992b 100644 --- a/packages/client/lib/commands/HGET.ts +++ b/packages/client/lib/commands/HGET.ts @@ -4,6 +4,12 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets the value of a field in a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param field - Field to get the value of + */ parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { parser.push('HGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts index 8d53669cdd4..13238ab6ea3 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -5,6 +5,11 @@ import { transformTuplesReply } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets all fields and values in a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HGETALL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HGETDEL.ts b/packages/client/lib/commands/HGETDEL.ts index a0326c425ea..8b55cae3ed5 100644 --- a/packages/client/lib/commands/HGETDEL.ts +++ b/packages/client/lib/commands/HGETDEL.ts @@ -3,6 +3,12 @@ import { RedisVariadicArgument } from './generic-transformers'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { + /** + * Gets and deletes the specified fields from a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param fields - Fields to get and delete + */ parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { parser.push('HGETDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HGETEX.ts b/packages/client/lib/commands/HGETEX.ts index ce265e15bd6..6b039575a27 100644 --- a/packages/client/lib/commands/HGETEX.ts +++ b/packages/client/lib/commands/HGETEX.ts @@ -12,6 +12,13 @@ export interface HGetExOptions { } export default { + /** + * Gets the values of the specified fields in a hash and optionally sets their expiration + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param fields - Fields to get values from + * @param options - Options for setting expiration + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HINCRBY.ts b/packages/client/lib/commands/HINCRBY.ts index 3638e408f7d..cb028315f4c 100644 --- a/packages/client/lib/commands/HINCRBY.ts +++ b/packages/client/lib/commands/HINCRBY.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Increments the integer value of a field in a hash by the given number + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param field - Field to increment + * @param increment - Increment amount + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HINCRBYFLOAT.ts b/packages/client/lib/commands/HINCRBYFLOAT.ts index 6d527583c71..6d85fa50432 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { + /** + * Increments the float value of a field in a hash by the given amount + * @param parser - The Redis command parser + * @param key - Key of the hash + * @param field - Field to increment + * @param increment - Increment amount (float) + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HKEYS.ts b/packages/client/lib/commands/HKEYS.ts index f07a1ac127f..bf2783eb2dc 100644 --- a/packages/client/lib/commands/HKEYS.ts +++ b/packages/client/lib/commands/HKEYS.ts @@ -4,6 +4,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets all field names in a hash + * @param parser - The Redis command parser + * @param key - Key of the hash + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HKEYS') parser.pushKey(key); diff --git a/packages/client/lib/commands/HLEN.ts b/packages/client/lib/commands/HLEN.ts index e3b89da3e7d..7ffbdeee9d6 100644 --- a/packages/client/lib/commands/HLEN.ts +++ b/packages/client/lib/commands/HLEN.ts @@ -4,6 +4,11 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets the number of fields in a hash. + * @param parser - The Redis command parser. + * @param key - Key of the hash. + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HMGET.ts b/packages/client/lib/commands/HMGET.ts index 51ba937339f..18a7baa219e 100644 --- a/packages/client/lib/commands/HMGET.ts +++ b/packages/client/lib/commands/HMGET.ts @@ -5,6 +5,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets the values of all the specified fields in a hash. + * @param parser - The Redis command parser. + * @param key - Key of the hash. + * @param fields - Fields to get from the hash. + */ parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { parser.push('HMGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts index fd0f320e65a..00ab1f4b4b5 100644 --- a/packages/client/lib/commands/HPERSIST.ts +++ b/packages/client/lib/commands/HPERSIST.ts @@ -3,6 +3,12 @@ import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../R import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Removes the expiration from the specified fields in a hash. + * @param parser - The Redis command parser. + * @param key - Key of the hash. + * @param fields - Fields to remove expiration from. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts index 34513c34e3b..2e20c96bb13 100644 --- a/packages/client/lib/commands/HPEXPIRE.ts +++ b/packages/client/lib/commands/HPEXPIRE.ts @@ -4,6 +4,15 @@ import { RedisVariadicArgument } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; export default { + /** + * Parses the arguments for the `HPEXPIRE` command. + * + * @param parser - The command parser instance. + * @param key - The key of the hash. + * @param fields - The fields to set the expiration for. + * @param ms - The expiration time in milliseconds. + * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT'). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts index 14288d7ae90..58fedc84765 100644 --- a/packages/client/lib/commands/HPEXPIREAT.ts +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -5,6 +5,15 @@ import { HashExpiration } from './HEXPIRE'; export default { IS_READ_ONLY: true, + /** + * Parses the arguments for the `HPEXPIREAT` command. + * + * @param parser - The command parser instance. + * @param key - The key of the hash. + * @param fields - The fields to set the expiration for. + * @param timestamp - The expiration timestamp (Unix timestamp or Date object). + * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT'). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts index cacce25a85f..d27a15749ae 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the HPEXPIRETIME command + * + * @param parser - The command parser + * @param key - The key to retrieve expiration time for + * @param fields - The fields to retrieve expiration time for + * @see https://redis.io/commands/hpexpiretime/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts index b9cd54a850d..f177e6b5a02 100644 --- a/packages/client/lib/commands/HPTTL.ts +++ b/packages/client/lib/commands/HPTTL.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the HPTTL command + * + * @param parser - The command parser + * @param key - The key to check time-to-live for + * @param fields - The fields to check time-to-live for + * @see https://redis.io/commands/hpttl/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HRANDFIELD.ts b/packages/client/lib/commands/HRANDFIELD.ts index 3383b94dcb2..38cc34dcee5 100644 --- a/packages/client/lib/commands/HRANDFIELD.ts +++ b/packages/client/lib/commands/HRANDFIELD.ts @@ -3,6 +3,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Constructs the HRANDFIELD command + * + * @param parser - The command parser + * @param key - The key of the hash to get a random field from + * @see https://redis.io/commands/hrandfield/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.ts index 62abe97e350..99c5cb0dada 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts @@ -3,6 +3,14 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { IS_READ_ONLY: true, + /** + * Constructs the HRANDFIELD command with count parameter + * + * @param parser - The command parser + * @param key - The key of the hash to get random fields from + * @param count - The number of fields to return (positive: unique fields, negative: may repeat fields) + * @see https://redis.io/commands/hrandfield/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts index aa8ebad1b93..e247006c6a7 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -8,6 +8,14 @@ export type HRandFieldCountWithValuesReply = Array<{ export default { IS_READ_ONLY: true, + /** + * Constructs the HRANDFIELD command with count parameter and WITHVALUES option + * + * @param parser - The command parser + * @param key - The key of the hash to get random fields from + * @param count - The number of fields to return (positive: unique fields, negative: may repeat fields) + * @see https://redis.io/commands/hrandfield/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts index e1e40663a07..78141814ff1 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -9,6 +9,15 @@ export interface HScanEntry { export default { IS_READ_ONLY: true, + /** + * Constructs the HSCAN command + * + * @param parser - The command parser + * @param key - The key of the hash to scan + * @param cursor - The cursor position to start scanning from + * @param options - Options for the scan (COUNT, MATCH, TYPE) + * @see https://redis.io/commands/hscan/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts index eff61a7aab0..8f7afe52b8e 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -3,6 +3,12 @@ import HSCAN from './HSCAN'; export default { IS_READ_ONLY: true, + /** + * Constructs the HSCAN command with NOVALUES option + * + * @param args - The same parameters as HSCAN command + * @see https://redis.io/commands/hscan/ + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/HSET.ts b/packages/client/lib/commands/HSET.ts index 1f50aeacf03..7dc4da8d3cf 100644 --- a/packages/client/lib/commands/HSET.ts +++ b/packages/client/lib/commands/HSET.ts @@ -18,6 +18,15 @@ type MultipleFieldsArguments = [...generic: GenericArguments, value: HSETObject export type HSETArguments = SingleFieldArguments | MultipleFieldsArguments; export default { + /** + * Constructs the HSET command + * + * @param parser - The command parser + * @param key - The key of the hash + * @param value - Either the field name (when using single field) or an object/map/array of field-value pairs + * @param fieldValue - The value to set (only used with single field variant) + * @see https://redis.io/commands/hset/ + */ parseCommand(parser: CommandParser, ...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) { parser.push('HSET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts index 3827538934c..316b95a91c3 100644 --- a/packages/client/lib/commands/HSETEX.ts +++ b/packages/client/lib/commands/HSETEX.ts @@ -20,6 +20,15 @@ type HSETEXMap = Map; type HSETEXTuples = Array<[HashTypes, HashTypes]> | Array; export default { + /** + * Constructs the HSETEX command + * + * @param parser - The command parser + * @param key - The key of the hash + * @param fields - Object, Map, or Array of field-value pairs to set + * @param options - Optional configuration for expiration and mode settings + * @see https://redis.io/commands/hsetex/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HSETNX.ts b/packages/client/lib/commands/HSETNX.ts index 130d7cd81d3..dc10b6c5e00 100644 --- a/packages/client/lib/commands/HSETNX.ts +++ b/packages/client/lib/commands/HSETNX.ts @@ -3,6 +3,15 @@ import { RedisArgument, Command, NumberReply } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the HSETNX command + * + * @param parser - The command parser + * @param key - The key of the hash + * @param field - The field to set if it does not exist + * @param value - The value to set + * @see https://redis.io/commands/hsetnx/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HSTRLEN.ts b/packages/client/lib/commands/HSTRLEN.ts index 2468747d4c9..016c14e27a8 100644 --- a/packages/client/lib/commands/HSTRLEN.ts +++ b/packages/client/lib/commands/HSTRLEN.ts @@ -4,6 +4,14 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the HSTRLEN command + * + * @param parser - The command parser + * @param key - The key of the hash + * @param field - The field to get the string length of + * @see https://redis.io/commands/hstrlen/ + */ parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { parser.push('HSTRLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts index 4b8fe5d7e85..710b4c7c1ff 100644 --- a/packages/client/lib/commands/HTTL.ts +++ b/packages/client/lib/commands/HTTL.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Returns the remaining time to live of field(s) in a hash. + * @param parser - The Redis command parser. + * @param key - Key of the hash. + * @param fields - Fields to check time to live. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HVALS.ts b/packages/client/lib/commands/HVALS.ts index ab17e47f533..faa5fe43442 100644 --- a/packages/client/lib/commands/HVALS.ts +++ b/packages/client/lib/commands/HVALS.ts @@ -4,6 +4,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Gets all values in a hash. + * @param parser - The Redis command parser. + * @param key - Key of the hash. + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HVALS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/INCR.ts b/packages/client/lib/commands/INCR.ts index e719f06bc19..0a294ccdc5a 100644 --- a/packages/client/lib/commands/INCR.ts +++ b/packages/client/lib/commands/INCR.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the INCR command + * + * @param parser - The command parser + * @param key - The key to increment + * @see https://redis.io/commands/incr/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('INCR'); parser.pushKey(key); diff --git a/packages/client/lib/commands/INCRBY.ts b/packages/client/lib/commands/INCRBY.ts index bf463185044..f23ec1a74a4 100644 --- a/packages/client/lib/commands/INCRBY.ts +++ b/packages/client/lib/commands/INCRBY.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the INCRBY command + * + * @param parser - The command parser + * @param key - The key to increment + * @param increment - The amount to increment by + * @see https://redis.io/commands/incrby/ + */ parseCommand(parser: CommandParser, key: RedisArgument, increment: number) { parser.push('INCRBY'); parser.pushKey(key); diff --git a/packages/client/lib/commands/INCRBYFLOAT.ts b/packages/client/lib/commands/INCRBYFLOAT.ts index 9a2dba42a6e..9effa756db5 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { + /** + * Constructs the INCRBYFLOAT command + * + * @param parser - The command parser + * @param key - The key to increment + * @param increment - The floating-point value to increment by + * @see https://redis.io/commands/incrbyfloat/ + */ parseCommand(parser: CommandParser, key: RedisArgument, increment: number) { parser.push('INCRBYFLOAT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/INFO.ts b/packages/client/lib/commands/INFO.ts index 82cbd497a5b..799fcb1825a 100644 --- a/packages/client/lib/commands/INFO.ts +++ b/packages/client/lib/commands/INFO.ts @@ -4,6 +4,13 @@ import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the INFO command + * + * @param parser - The command parser + * @param section - Optional specific section of information to retrieve + * @see https://redis.io/commands/info/ + */ parseCommand(parser: CommandParser, section?: RedisArgument) { parser.push('INFO'); diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts index e516245d2ee..eb240c26ceb 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -4,6 +4,13 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the KEYS command + * + * @param parser - The command parser + * @param pattern - The pattern to match keys against + * @see https://redis.io/commands/keys/ + */ parseCommand(parser: CommandParser, pattern: RedisArgument) { parser.push('KEYS', pattern); }, diff --git a/packages/client/lib/commands/LASTSAVE.ts b/packages/client/lib/commands/LASTSAVE.ts index 447cb95ab6d..fbbc6a0046a 100644 --- a/packages/client/lib/commands/LASTSAVE.ts +++ b/packages/client/lib/commands/LASTSAVE.ts @@ -4,6 +4,12 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LASTSAVE command + * + * @param parser - The command parser + * @see https://redis.io/commands/lastsave/ + */ parseCommand(parser: CommandParser) { parser.push('LASTSAVE'); }, diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.ts b/packages/client/lib/commands/LATENCY_DOCTOR.ts index 49c830b3065..5ba7ee6a7bf 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts @@ -4,6 +4,12 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LATENCY DOCTOR command + * + * @param parser - The command parser + * @see https://redis.io/commands/latency-doctor/ + */ parseCommand(parser: CommandParser) { parser.push('LATENCY', 'DOCTOR'); }, diff --git a/packages/client/lib/commands/LATENCY_GRAPH.ts b/packages/client/lib/commands/LATENCY_GRAPH.ts index 20251c3cded..8c53624c741 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.ts @@ -25,6 +25,13 @@ export type LatencyEvent = typeof LATENCY_EVENTS[keyof typeof LATENCY_EVENTS]; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LATENCY GRAPH command + * + * @param parser - The command parser + * @param event - The latency event to get the graph for + * @see https://redis.io/commands/latency-graph/ + */ parseCommand(parser: CommandParser, event: LatencyEvent) { parser.push('LATENCY', 'GRAPH', event); }, diff --git a/packages/client/lib/commands/LATENCY_HISTORY.ts b/packages/client/lib/commands/LATENCY_HISTORY.ts index 6e0e4d5c560..dec7129befa 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.ts @@ -23,6 +23,13 @@ export type LatencyEventType = ( export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LATENCY HISTORY command + * + * @param parser - The command parser + * @param event - The latency event to get the history for + * @see https://redis.io/commands/latency-history/ + */ parseCommand(parser: CommandParser, event: LatencyEventType) { parser.push('LATENCY', 'HISTORY', event); }, diff --git a/packages/client/lib/commands/LATENCY_LATEST.ts b/packages/client/lib/commands/LATENCY_LATEST.ts index 2ce3efd291c..8fbdd46a13a 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.ts @@ -4,6 +4,12 @@ import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LATENCY LATEST command + * + * @param parser - The command parser + * @see https://redis.io/commands/latency-latest/ + */ parseCommand(parser: CommandParser) { parser.push('LATENCY', 'LATEST'); }, diff --git a/packages/client/lib/commands/LCS.ts b/packages/client/lib/commands/LCS.ts index ed4f11ad990..9b2317e147f 100644 --- a/packages/client/lib/commands/LCS.ts +++ b/packages/client/lib/commands/LCS.ts @@ -3,6 +3,14 @@ import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the LCS command (Longest Common Substring) + * + * @param parser - The command parser + * @param key1 - First key containing the first string + * @param key2 - Second key containing the second string + * @see https://redis.io/commands/lcs/ + */ parseCommand( parser: CommandParser, key1: RedisArgument, diff --git a/packages/client/lib/commands/LCS_IDX.ts b/packages/client/lib/commands/LCS_IDX.ts index cb0a6b07657..684aa99efb0 100644 --- a/packages/client/lib/commands/LCS_IDX.ts +++ b/packages/client/lib/commands/LCS_IDX.ts @@ -25,6 +25,15 @@ export type LcsIdxReply = TuplesToMapReply<[ export default { IS_READ_ONLY: LCS.IS_READ_ONLY, + /** + * Constructs the LCS command with IDX option + * + * @param parser - The command parser + * @param key1 - First key containing the first string + * @param key2 - Second key containing the second string + * @param options - Additional options for the LCS IDX command + * @see https://redis.io/commands/lcs/ + */ parseCommand( parser: CommandParser, key1: RedisArgument, diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts index d2a743983e1..f3578b789fc 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts @@ -16,6 +16,12 @@ export type LcsIdxWithMatchLenReply = TuplesToMapReply<[ export default { IS_READ_ONLY: LCS_IDX.IS_READ_ONLY, + /** + * Constructs the LCS command with IDX and WITHMATCHLEN options + * + * @param args - The same parameters as LCS_IDX command + * @see https://redis.io/commands/lcs/ + */ parseCommand(...args: Parameters) { const parser = args[0]; LCS_IDX.parseCommand(...args); diff --git a/packages/client/lib/commands/LCS_LEN.ts b/packages/client/lib/commands/LCS_LEN.ts index a1f92d914a4..bb35c3d9209 100644 --- a/packages/client/lib/commands/LCS_LEN.ts +++ b/packages/client/lib/commands/LCS_LEN.ts @@ -3,6 +3,12 @@ import LCS from './LCS'; export default { IS_READ_ONLY: LCS.IS_READ_ONLY, + /** + * Constructs the LCS command with LEN option + * + * @param args - The same parameters as LCS command + * @see https://redis.io/commands/lcs/ + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts index 6335fc40c2c..dd7671a41c6 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -4,6 +4,14 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the LINDEX command + * + * @param parser - The command parser + * @param key - The key of the list + * @param index - The index of the element to retrieve + * @see https://redis.io/commands/lindex/ + */ parseCommand(parser: CommandParser, key: RedisArgument, index: number) { parser.push('LINDEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LINSERT.ts b/packages/client/lib/commands/LINSERT.ts index 8a40ac66630..ede230191ba 100644 --- a/packages/client/lib/commands/LINSERT.ts +++ b/packages/client/lib/commands/LINSERT.ts @@ -5,6 +5,16 @@ type LInsertPosition = 'BEFORE' | 'AFTER'; export default { IS_READ_ONLY: true, + /** + * Constructs the LINSERT command + * + * @param parser - The command parser + * @param key - The key of the list + * @param position - The position where to insert (BEFORE or AFTER) + * @param pivot - The element to find in the list + * @param element - The element to insert + * @see https://redis.io/commands/linsert/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LLEN.ts b/packages/client/lib/commands/LLEN.ts index 674e022e60d..7ece6823bb5 100644 --- a/packages/client/lib/commands/LLEN.ts +++ b/packages/client/lib/commands/LLEN.ts @@ -4,6 +4,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the LLEN command + * + * @param parser - The command parser + * @param key - The key of the list to get the length of + * @see https://redis.io/commands/llen/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('LLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LMOVE.ts b/packages/client/lib/commands/LMOVE.ts index f3ac847e900..9ed0003b23d 100644 --- a/packages/client/lib/commands/LMOVE.ts +++ b/packages/client/lib/commands/LMOVE.ts @@ -4,6 +4,16 @@ import { ListSide } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the LMOVE command + * + * @param parser - The command parser + * @param source - The source list key + * @param destination - The destination list key + * @param sourceSide - The side to pop from (LEFT or RIGHT) + * @param destinationSide - The side to push to (LEFT or RIGHT) + * @see https://redis.io/commands/lmove/ + */ parseCommand( parser: CommandParser, source: RedisArgument, diff --git a/packages/client/lib/commands/LMPOP.ts b/packages/client/lib/commands/LMPOP.ts index c8095e42e75..54dc40c1c3d 100644 --- a/packages/client/lib/commands/LMPOP.ts +++ b/packages/client/lib/commands/LMPOP.ts @@ -24,6 +24,13 @@ export type LMPopArguments = Tail>; export default { IS_READ_ONLY: false, + /** + * Constructs the LMPOP command + * + * @param parser - The command parser + * @param args - Arguments including keys, side (LEFT or RIGHT), and options + * @see https://redis.io/commands/lmpop/ + */ parseCommand(parser: CommandParser, ...args: LMPopArguments) { parser.push('LMPOP'); parseLMPopArguments(parser, ...args); diff --git a/packages/client/lib/commands/LOLWUT.ts b/packages/client/lib/commands/LOLWUT.ts index 372bf536967..5e07a103720 100644 --- a/packages/client/lib/commands/LOLWUT.ts +++ b/packages/client/lib/commands/LOLWUT.ts @@ -4,6 +4,14 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the LOLWUT command + * + * @param parser - The command parser + * @param version - Optional version parameter + * @param optionalArguments - Additional optional numeric arguments + * @see https://redis.io/commands/lolwut/ + */ parseCommand(parser: CommandParser, version?: number, ...optionalArguments: Array) { parser.push('LOLWUT'); if (version) { diff --git a/packages/client/lib/commands/LPOP.ts b/packages/client/lib/commands/LPOP.ts index 3125236bfa0..aaa83be465d 100644 --- a/packages/client/lib/commands/LPOP.ts +++ b/packages/client/lib/commands/LPOP.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { + /** + * Constructs the LPOP command + * + * @param parser - The command parser + * @param key - The key of the list to pop from + * @see https://redis.io/commands/lpop/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('LPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LPOP_COUNT.ts b/packages/client/lib/commands/LPOP_COUNT.ts index 6d9aba42c21..cdc0dc41a22 100644 --- a/packages/client/lib/commands/LPOP_COUNT.ts +++ b/packages/client/lib/commands/LPOP_COUNT.ts @@ -4,6 +4,14 @@ import LPOP from './LPOP'; export default { IS_READ_ONLY: false, + /** + * Constructs the LPOP command with count parameter + * + * @param parser - The command parser + * @param key - The key of the list to pop from + * @param count - The number of elements to pop + * @see https://redis.io/commands/lpop/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { LPOP.parseCommand(parser, key); parser.push(count.toString()) diff --git a/packages/client/lib/commands/LPOS.ts b/packages/client/lib/commands/LPOS.ts index bb05ba6555d..54078b8185f 100644 --- a/packages/client/lib/commands/LPOS.ts +++ b/packages/client/lib/commands/LPOS.ts @@ -9,6 +9,15 @@ export interface LPosOptions { export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the LPOS command + * + * @param parser - The command parser + * @param key - The key of the list + * @param element - The element to search for + * @param options - Optional parameters for RANK and MAXLEN + * @see https://redis.io/commands/lpos/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LPOS_COUNT.ts b/packages/client/lib/commands/LPOS_COUNT.ts index e782a2d26ee..ace6e49c1e5 100644 --- a/packages/client/lib/commands/LPOS_COUNT.ts +++ b/packages/client/lib/commands/LPOS_COUNT.ts @@ -5,6 +5,16 @@ import LPOS, { LPosOptions } from './LPOS'; export default { CACHEABLE: LPOS.CACHEABLE, IS_READ_ONLY: LPOS.IS_READ_ONLY, + /** + * Constructs the LPOS command with COUNT option + * + * @param parser - The command parser + * @param key - The key of the list + * @param element - The element to search for + * @param count - The number of positions to return + * @param options - Optional parameters for RANK and MAXLEN + * @see https://redis.io/commands/lpos/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LPUSH.ts b/packages/client/lib/commands/LPUSH.ts index 293029034ee..89e1e094870 100644 --- a/packages/client/lib/commands/LPUSH.ts +++ b/packages/client/lib/commands/LPUSH.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the LPUSH command + * + * @param parser - The command parser + * @param key - The key of the list + * @param elements - One or more elements to push to the list + * @see https://redis.io/commands/lpush/ + */ parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) { parser.push('LPUSH'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LPUSHX.ts b/packages/client/lib/commands/LPUSHX.ts index 98dd51a7ac2..e87bd4ff0d5 100644 --- a/packages/client/lib/commands/LPUSHX.ts +++ b/packages/client/lib/commands/LPUSHX.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the LPUSHX command + * + * @param parser - The command parser + * @param key - The key of the list + * @param elements - One or more elements to push to the list if it exists + * @see https://redis.io/commands/lpushx/ + */ parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) { parser.push('LPUSHX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LRANGE.ts b/packages/client/lib/commands/LRANGE.ts index ab033dd88a4..040bb6b4498 100644 --- a/packages/client/lib/commands/LRANGE.ts +++ b/packages/client/lib/commands/LRANGE.ts @@ -4,6 +4,15 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the LRANGE command + * + * @param parser - The command parser + * @param key - The key of the list + * @param start - The starting index + * @param stop - The ending index + * @see https://redis.io/commands/lrange/ + */ parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) { parser.push('LRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LREM.ts b/packages/client/lib/commands/LREM.ts index bb97e3882e7..4e5de0fa78c 100644 --- a/packages/client/lib/commands/LREM.ts +++ b/packages/client/lib/commands/LREM.ts @@ -3,6 +3,15 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the LREM command + * + * @param parser - The command parser + * @param key - The key of the list + * @param count - The count of elements to remove (negative: from tail to head, 0: all occurrences, positive: from head to tail) + * @param element - The element to remove + * @see https://redis.io/commands/lrem/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number, element: RedisArgument) { parser.push('LREM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LSET.ts b/packages/client/lib/commands/LSET.ts index 0fe646fbb73..052961a316e 100644 --- a/packages/client/lib/commands/LSET.ts +++ b/packages/client/lib/commands/LSET.ts @@ -3,6 +3,15 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the LSET command + * + * @param parser - The command parser + * @param key - The key of the list + * @param index - The index of the element to replace + * @param element - The new value to set + * @see https://redis.io/commands/lset/ + */ parseCommand(parser: CommandParser, key: RedisArgument, index: number, element: RedisArgument) { parser.push('LSET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LTRIM.ts b/packages/client/lib/commands/LTRIM.ts index acc7e767d0d..31c2b66b5a9 100644 --- a/packages/client/lib/commands/LTRIM.ts +++ b/packages/client/lib/commands/LTRIM.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { + /** + * Constructs the LTRIM command + * + * @param parser - The command parser + * @param key - The key of the list + * @param start - The starting index + * @param stop - The ending index + * @see https://redis.io/commands/ltrim/ + */ parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) { parser.push('LTRIM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.ts b/packages/client/lib/commands/MEMORY_DOCTOR.ts index 3a2d808db10..21e42ccc7ea 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts @@ -4,6 +4,12 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the MEMORY DOCTOR command + * + * @param parser - The command parser + * @see https://redis.io/commands/memory-doctor/ + */ parseCommand(parser: CommandParser) { parser.push('MEMORY', 'DOCTOR'); }, diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts index af6b5db3347..69ad8c37a85 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts @@ -4,6 +4,12 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the MEMORY MALLOC-STATS command + * + * @param parser - The command parser + * @see https://redis.io/commands/memory-malloc-stats/ + */ parseCommand(parser: CommandParser) { parser.push('MEMORY', 'MALLOC-STATS'); }, diff --git a/packages/client/lib/commands/MEMORY_PURGE.ts b/packages/client/lib/commands/MEMORY_PURGE.ts index bbd02890786..39f837016ad 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Constructs the MEMORY PURGE command + * + * @param parser - The command parser + * @see https://redis.io/commands/memory-purge/ + */ parseCommand(parser: CommandParser) { parser.push('MEMORY', 'PURGE'); }, diff --git a/packages/client/lib/commands/MEMORY_STATS.ts b/packages/client/lib/commands/MEMORY_STATS.ts index 33410535aa9..9391a91613c 100644 --- a/packages/client/lib/commands/MEMORY_STATS.ts +++ b/packages/client/lib/commands/MEMORY_STATS.ts @@ -38,6 +38,12 @@ export type MemoryStatsReply = TuplesToMapReply<[ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the MEMORY STATS command + * + * @param parser - The command parser + * @see https://redis.io/commands/memory-stats/ + */ parseCommand(parser: CommandParser) { parser.push('MEMORY', 'STATS'); }, diff --git a/packages/client/lib/commands/MEMORY_USAGE.ts b/packages/client/lib/commands/MEMORY_USAGE.ts index 6e85438dbed..a1fa79f6210 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.ts @@ -7,6 +7,14 @@ export interface MemoryUsageOptions { export default { IS_READ_ONLY: true, + /** + * Constructs the MEMORY USAGE command + * + * @param parser - The command parser + * @param key - The key to get memory usage for + * @param options - Optional parameters including SAMPLES + * @see https://redis.io/commands/memory-usage/ + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: MemoryUsageOptions) { parser.push('MEMORY', 'USAGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/MGET.ts b/packages/client/lib/commands/MGET.ts index ce1e9ba7781..22145dd3485 100644 --- a/packages/client/lib/commands/MGET.ts +++ b/packages/client/lib/commands/MGET.ts @@ -4,6 +4,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the MGET command + * + * @param parser - The command parser + * @param keys - Array of keys to get + * @see https://redis.io/commands/mget/ + */ parseCommand(parser: CommandParser, keys: Array) { parser.push('MGET'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/MIGRATE.ts b/packages/client/lib/commands/MIGRATE.ts index 15345060aa7..ba798e331ab 100644 --- a/packages/client/lib/commands/MIGRATE.ts +++ b/packages/client/lib/commands/MIGRATE.ts @@ -10,6 +10,18 @@ export interface MigrateOptions { export default { IS_READ_ONLY: false, + /** + * Constructs the MIGRATE command + * + * @param parser - The command parser + * @param host - Target Redis instance host + * @param port - Target Redis instance port + * @param key - Key or keys to migrate + * @param destinationDb - Target database index + * @param timeout - Timeout in milliseconds + * @param options - Optional parameters including COPY, REPLACE, and AUTH + * @see https://redis.io/commands/migrate/ + */ parseCommand( parser: CommandParser, host: RedisArgument, diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts index 85203138f57..8183c419a66 100644 --- a/packages/client/lib/commands/MODULE_LIST.ts +++ b/packages/client/lib/commands/MODULE_LIST.ts @@ -9,6 +9,12 @@ export type ModuleListReply = ArrayReply) { parser.push('MODULE', 'LOAD', path); diff --git a/packages/client/lib/commands/MODULE_UNLOAD.ts b/packages/client/lib/commands/MODULE_UNLOAD.ts index 1acc359d0d4..6d19b2b2a73 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.ts @@ -4,6 +4,13 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the MODULE UNLOAD command + * + * @param parser - The command parser + * @param name - The name of the module to unload + * @see https://redis.io/commands/module-unload/ + */ parseCommand(parser: CommandParser, name: RedisArgument) { parser.push('MODULE', 'UNLOAD', name); }, diff --git a/packages/client/lib/commands/MOVE.ts b/packages/client/lib/commands/MOVE.ts index 8a6c5427fbc..0c08a6fa100 100644 --- a/packages/client/lib/commands/MOVE.ts +++ b/packages/client/lib/commands/MOVE.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the MOVE command + * + * @param parser - The command parser + * @param key - The key to move + * @param db - The destination database index + * @see https://redis.io/commands/move/ + */ parseCommand(parser: CommandParser, key: RedisArgument, db: number) { parser.push('MOVE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/MSET.ts b/packages/client/lib/commands/MSET.ts index f761854f09c..ab734bae5c7 100644 --- a/packages/client/lib/commands/MSET.ts +++ b/packages/client/lib/commands/MSET.ts @@ -33,6 +33,13 @@ export function parseMSetArguments(parser: CommandParser, toSet: MSetArguments) export default { IS_READ_ONLY: true, + /** + * Constructs the MSET command + * + * @param parser - The command parser + * @param toSet - Key-value pairs to set (array of tuples, flat array, or object) + * @see https://redis.io/commands/mset/ + */ parseCommand(parser: CommandParser, toSet: MSetArguments) { parser.push('MSET'); return parseMSetArguments(parser, toSet); diff --git a/packages/client/lib/commands/MSETNX.ts b/packages/client/lib/commands/MSETNX.ts index 3ecce9525de..9a2186023f6 100644 --- a/packages/client/lib/commands/MSETNX.ts +++ b/packages/client/lib/commands/MSETNX.ts @@ -4,6 +4,13 @@ import { MSetArguments, parseMSetArguments } from './MSET'; export default { IS_READ_ONLY: true, + /** + * Constructs the MSETNX command + * + * @param parser - The command parser + * @param toSet - Key-value pairs to set if none of the keys exist (array of tuples, flat array, or object) + * @see https://redis.io/commands/msetnx/ + */ parseCommand(parser: CommandParser, toSet: MSetArguments) { parser.push('MSETNX'); return parseMSetArguments(parser, toSet); diff --git a/packages/client/lib/commands/OBJECT_ENCODING.ts b/packages/client/lib/commands/OBJECT_ENCODING.ts index 3a795f6fb64..2c0f6b41bbc 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.ts @@ -3,6 +3,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Constructs the OBJECT ENCODING command + * + * @param parser - The command parser + * @param key - The key to get the internal encoding for + * @see https://redis.io/commands/object-encoding/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'ENCODING'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_FREQ.ts b/packages/client/lib/commands/OBJECT_FREQ.ts index dad1124b101..42a310a97c5 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the OBJECT FREQ command + * + * @param parser - The command parser + * @param key - The key to get the access frequency for + * @see https://redis.io/commands/object-freq/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'FREQ'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.ts b/packages/client/lib/commands/OBJECT_IDLETIME.ts index 2bd32f4e65d..2d4afeda65a 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the OBJECT IDLETIME command + * + * @param parser - The command parser + * @param key - The key to get the idle time for + * @see https://redis.io/commands/object-idletime/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'IDLETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.ts index 4bee4dea60c..7948a4941de 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the OBJECT REFCOUNT command + * + * @param parser - The command parser + * @param key - The key to get the reference count for + * @see https://redis.io/commands/object-refcount/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'REFCOUNT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PERSIST.ts b/packages/client/lib/commands/PERSIST.ts index a1d31523664..3b1f4a7062c 100644 --- a/packages/client/lib/commands/PERSIST.ts +++ b/packages/client/lib/commands/PERSIST.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the PERSIST command + * + * @param parser - The command parser + * @param key - The key to remove the expiration from + * @see https://redis.io/commands/persist/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('PERSIST'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PEXPIRE.ts b/packages/client/lib/commands/PEXPIRE.ts index 4053f46c8e2..f1d96076885 100644 --- a/packages/client/lib/commands/PEXPIRE.ts +++ b/packages/client/lib/commands/PEXPIRE.ts @@ -3,6 +3,15 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the PEXPIRE command + * + * @param parser - The command parser + * @param key - The key to set the expiration for + * @param ms - The expiration time in milliseconds + * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT') + * @see https://redis.io/commands/pexpire/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/PEXPIREAT.ts b/packages/client/lib/commands/PEXPIREAT.ts index e454447c970..072cc33bbb4 100644 --- a/packages/client/lib/commands/PEXPIREAT.ts +++ b/packages/client/lib/commands/PEXPIREAT.ts @@ -4,6 +4,15 @@ import { transformPXAT } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the PEXPIREAT command + * + * @param parser - The command parser + * @param key - The key to set the expiration for + * @param msTimestamp - The expiration timestamp in milliseconds (Unix timestamp or Date object) + * @param mode - Optional mode for the command ('NX', 'XX', 'GT', 'LT') + * @see https://redis.io/commands/pexpireat/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/PEXPIRETIME.ts b/packages/client/lib/commands/PEXPIRETIME.ts index b5d04eae230..6b3488662c3 100644 --- a/packages/client/lib/commands/PEXPIRETIME.ts +++ b/packages/client/lib/commands/PEXPIRETIME.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the PEXPIRETIME command + * + * @param parser - The command parser + * @param key - The key to get the expiration time for in milliseconds + * @see https://redis.io/commands/pexpiretime/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('PEXPIRETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PFADD.ts b/packages/client/lib/commands/PFADD.ts index 94c2d1d5ae6..f5d2a280ca0 100644 --- a/packages/client/lib/commands/PFADD.ts +++ b/packages/client/lib/commands/PFADD.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the PFADD command + * + * @param parser - The command parser + * @param key - The key of the HyperLogLog + * @param element - Optional elements to add + * @see https://redis.io/commands/pfadd/ + */ parseCommand(parser: CommandParser, key: RedisArgument, element?: RedisVariadicArgument) { parser.push('PFADD') parser.pushKey(key); diff --git a/packages/client/lib/commands/PFCOUNT.ts b/packages/client/lib/commands/PFCOUNT.ts index 46d2e2ed71f..1358fed7d67 100644 --- a/packages/client/lib/commands/PFCOUNT.ts +++ b/packages/client/lib/commands/PFCOUNT.ts @@ -4,6 +4,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the PFCOUNT command + * + * @param parser - The command parser + * @param keys - One or more keys of HyperLogLog structures to count + * @see https://redis.io/commands/pfcount/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('PFCOUNT'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/PFMERGE.ts b/packages/client/lib/commands/PFMERGE.ts index e8eccf1afff..834a5dfbf55 100644 --- a/packages/client/lib/commands/PFMERGE.ts +++ b/packages/client/lib/commands/PFMERGE.ts @@ -3,6 +3,14 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the PFMERGE command + * + * @param parser - The command parser + * @param destination - The destination key to merge to + * @param sources - One or more source keys to merge from + * @see https://redis.io/commands/pfmerge/ + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/PING.ts b/packages/client/lib/commands/PING.ts index 26807eeeba4..1e8d21e1584 100644 --- a/packages/client/lib/commands/PING.ts +++ b/packages/client/lib/commands/PING.ts @@ -4,6 +4,13 @@ import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../R export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the PING command + * + * @param parser - The command parser + * @param message - Optional message to be returned instead of PONG + * @see https://redis.io/commands/ping/ + */ parseCommand(parser: CommandParser, message?: RedisArgument) { parser.push('PING'); if (message) { diff --git a/packages/client/lib/commands/PSETEX.ts b/packages/client/lib/commands/PSETEX.ts index 03a58546d67..5b6d83bd694 100644 --- a/packages/client/lib/commands/PSETEX.ts +++ b/packages/client/lib/commands/PSETEX.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { + /** + * Constructs the PSETEX command + * + * @param parser - The command parser + * @param key - The key to set + * @param ms - The expiration time in milliseconds + * @param value - The value to set + * @see https://redis.io/commands/psetex/ + */ parseCommand(parser: CommandParser, key: RedisArgument, ms: number, value: RedisArgument) { parser.push('PSETEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PTTL.ts b/packages/client/lib/commands/PTTL.ts index 5717c51179f..9d408aeee17 100644 --- a/packages/client/lib/commands/PTTL.ts +++ b/packages/client/lib/commands/PTTL.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the PTTL command + * + * @param parser - The command parser + * @param key - The key to get the time to live in milliseconds + * @see https://redis.io/commands/pttl/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('PTTL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index 557efd18834..197a2b069eb 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -5,6 +5,14 @@ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, IS_FORWARD_COMMAND: true, + /** + * Constructs the PUBLISH command + * + * @param parser - The command parser + * @param channel - The channel to publish to + * @param message - The message to publish + * @see https://redis.io/commands/publish/ + */ parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { parser.push('PUBLISH', channel, message); }, diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.ts index 0f53c79a78a..c9eb9bf7b4e 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts @@ -4,6 +4,13 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the PUBSUB CHANNELS command + * + * @param parser - The command parser + * @param pattern - Optional pattern to filter channels + * @see https://redis.io/commands/pubsub-channels/ + */ parseCommand(parser: CommandParser, pattern?: RedisArgument) { parser.push('PUBSUB', 'CHANNELS'); diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.ts index 173446e023b..4b876db88f1 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts @@ -4,6 +4,12 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the PUBSUB NUMPAT command + * + * @param parser - The command parser + * @see https://redis.io/commands/pubsub-numpat/ + */ parseCommand(parser: CommandParser) { parser.push('PUBSUB', 'NUMPAT'); }, diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.ts index cc74d5d8a73..da6647dc553 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts @@ -5,6 +5,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the PUBSUB NUMSUB command + * + * @param parser - The command parser + * @param channels - Optional channel names to get subscription count for + * @see https://redis.io/commands/pubsub-numsub/ + */ parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { parser.push('PUBSUB', 'NUMSUB'); @@ -12,6 +19,12 @@ export default { parser.pushVariadic(channels); } }, + /** + * Transforms the PUBSUB NUMSUB reply into a record of channel name to subscriber count + * + * @param rawReply - The raw reply from Redis + * @returns Record mapping channel names to their subscriber counts + */ transformReply(rawReply: UnwrapReply>) { const reply = Object.create(null); let i = 0; diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts index 46ac2005fc3..30601de55df 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts @@ -4,6 +4,13 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the PUBSUB SHARDCHANNELS command + * + * @param parser - The command parser + * @param pattern - Optional pattern to filter shard channels + * @see https://redis.io/commands/pubsub-shardchannels/ + */ parseCommand(parser: CommandParser, pattern?: RedisArgument) { parser.push('PUBSUB', 'SHARDCHANNELS'); diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts index 220eadeabe3..9d54a113d78 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts @@ -4,6 +4,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Constructs the PUBSUB SHARDNUMSUB command + * + * @param parser - The command parser + * @param channels - Optional shard channel names to get subscription count for + * @see https://redis.io/commands/pubsub-shardnumsub/ + */ parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { parser.push('PUBSUB', 'SHARDNUMSUB'); @@ -11,6 +18,12 @@ export default { parser.pushVariadic(channels); } }, + /** + * Transforms the PUBSUB SHARDNUMSUB reply into a record of shard channel name to subscriber count + * + * @param reply - The raw reply from Redis + * @returns Record mapping shard channel names to their subscriber counts + */ transformReply(reply: UnwrapReply>) { const transformedReply: Record = Object.create(null); diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts index 97d040a0d1d..263f539113b 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -4,6 +4,12 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the RANDOMKEY command + * + * @param parser - The command parser + * @see https://redis.io/commands/randomkey/ + */ parseCommand(parser: CommandParser) { parser.push('RANDOMKEY'); }, diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts index ce3300c5321..16eef975818 100644 --- a/packages/client/lib/commands/READONLY.ts +++ b/packages/client/lib/commands/READONLY.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the READONLY command + * + * @param parser - The command parser + * @see https://redis.io/commands/readonly/ + */ parseCommand(parser: CommandParser) { parser.push('READONLY'); }, diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts index 7d9d8c7e00a..f747366448c 100644 --- a/packages/client/lib/commands/READWRITE.ts +++ b/packages/client/lib/commands/READWRITE.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the READWRITE command + * + * @param parser - The command parser + * @see https://redis.io/commands/readwrite/ + */ parseCommand(parser: CommandParser) { parser.push('READWRITE'); }, diff --git a/packages/client/lib/commands/RENAME.ts b/packages/client/lib/commands/RENAME.ts index 245851ca31a..0033758d128 100644 --- a/packages/client/lib/commands/RENAME.ts +++ b/packages/client/lib/commands/RENAME.ts @@ -3,6 +3,14 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the RENAME command + * + * @param parser - The command parser + * @param key - The key to rename + * @param newKey - The new key name + * @see https://redis.io/commands/rename/ + */ parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { parser.push('RENAME'); parser.pushKeys([key, newKey]); diff --git a/packages/client/lib/commands/RENAMENX.ts b/packages/client/lib/commands/RENAMENX.ts index 0e8d4f73cf3..38c12dee727 100644 --- a/packages/client/lib/commands/RENAMENX.ts +++ b/packages/client/lib/commands/RENAMENX.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the RENAMENX command + * + * @param parser - The command parser + * @param key - The key to rename + * @param newKey - The new key name, if it doesn't exist + * @see https://redis.io/commands/renamenx/ + */ parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { parser.push('RENAMENX'); parser.pushKeys([key, newKey]); diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts index c4b09bc4fb8..08d4167fff4 100644 --- a/packages/client/lib/commands/REPLICAOF.ts +++ b/packages/client/lib/commands/REPLICAOF.ts @@ -4,6 +4,14 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the REPLICAOF command + * + * @param parser - The command parser + * @param host - The host of the master to replicate from + * @param port - The port of the master to replicate from + * @see https://redis.io/commands/replicaof/ + */ parseCommand(parser: CommandParser, host: string, port: number) { parser.push('REPLICAOF', host, port.toString()); }, diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts index e8de532b6a4..947ee9544d9 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the RESTORE-ASKING command + * + * @param parser - The command parser + * @see https://redis.io/commands/restore-asking/ + */ parseCommand(parser: CommandParser) { parser.push('RESTORE-ASKING'); }, diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts index 49016c525bd..5b07a773cc4 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -1,6 +1,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +/** + * Options for the RESTORE command + * + * @property REPLACE - Replace existing key + * @property ABSTTL - Use the TTL value as absolute timestamp + * @property IDLETIME - Set the idle time (seconds) for the key + * @property FREQ - Set the frequency counter for LFU policy + */ export interface RestoreOptions { REPLACE?: boolean; ABSTTL?: boolean; @@ -10,6 +18,16 @@ export interface RestoreOptions { export default { IS_READ_ONLY: false, + /** + * Constructs the RESTORE command + * + * @param parser - The command parser + * @param key - The key to restore + * @param ttl - Time to live in milliseconds, 0 for no expiry + * @param serializedValue - The serialized value from DUMP command + * @param options - Options for the RESTORE command + * @see https://redis.io/commands/restore/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ROLE.ts b/packages/client/lib/commands/ROLE.ts index f45bbad5c01..749ac4935fa 100644 --- a/packages/client/lib/commands/ROLE.ts +++ b/packages/client/lib/commands/ROLE.ts @@ -1,12 +1,18 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, NumberReply, ArrayReply, TuplesReply, UnwrapReply, Command } from '../RESP/types'; +/** + * Role information returned for a Redis master + */ type MasterRole = [ role: BlobStringReply<'master'>, replicationOffest: NumberReply, replicas: ArrayReply> ]; +/** + * Role information returned for a Redis slave + */ type SlaveRole = [ role: BlobStringReply<'slave'>, masterHost: BlobStringReply, @@ -15,19 +21,37 @@ type SlaveRole = [ dataReceived: NumberReply ]; +/** + * Role information returned for a Redis sentinel + */ type SentinelRole = [ role: BlobStringReply<'sentinel'>, masterNames: ArrayReply ]; +/** + * Combined role type for Redis instance role information + */ type Role = TuplesReply; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the ROLE command + * + * @param parser - The command parser + * @see https://redis.io/commands/role/ + */ parseCommand(parser: CommandParser) { parser.push('ROLE'); }, + /** + * Transforms the ROLE reply into a structured object + * + * @param reply - The raw reply from Redis + * @returns Structured object representing role information + */ transformReply(reply: UnwrapReply) { switch (reply[0] as unknown as UnwrapReply) { case 'master': { diff --git a/packages/client/lib/commands/RPOP.ts b/packages/client/lib/commands/RPOP.ts index 4cc105c3704..4e284496579 100644 --- a/packages/client/lib/commands/RPOP.ts +++ b/packages/client/lib/commands/RPOP.ts @@ -2,6 +2,13 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { + /** + * Constructs the RPOP command + * + * @param parser - The command parser + * @param key - The list key to pop from + * @see https://redis.io/commands/rpop/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('RPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/RPOPLPUSH.ts b/packages/client/lib/commands/RPOPLPUSH.ts index dcac0472235..936aeb01c8f 100644 --- a/packages/client/lib/commands/RPOPLPUSH.ts +++ b/packages/client/lib/commands/RPOPLPUSH.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { + /** + * Constructs the RPOPLPUSH command + * + * @param parser - The command parser + * @param source - The source list key + * @param destination - The destination list key + * @see https://redis.io/commands/rpoplpush/ + */ parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument) { parser.push('RPOPLPUSH'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/RPOP_COUNT.ts b/packages/client/lib/commands/RPOP_COUNT.ts index aff91c6a6f7..2a60335da94 100644 --- a/packages/client/lib/commands/RPOP_COUNT.ts +++ b/packages/client/lib/commands/RPOP_COUNT.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { + /** + * Constructs the RPOP command with count parameter + * + * @param parser - The command parser + * @param key - The list key to pop from + * @param count - The number of elements to pop + * @see https://redis.io/commands/rpop/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('RPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/RPUSH.ts b/packages/client/lib/commands/RPUSH.ts index b820aae6906..452623e7f0d 100644 --- a/packages/client/lib/commands/RPUSH.ts +++ b/packages/client/lib/commands/RPUSH.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the RPUSH command + * + * @param parser - The command parser + * @param key - The list key to push to + * @param element - One or more elements to push + * @see https://redis.io/commands/rpush/ + */ parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) { parser.push('RPUSH'); parser.pushKey(key); diff --git a/packages/client/lib/commands/RPUSHX.ts b/packages/client/lib/commands/RPUSHX.ts index 243f717bb78..a9ec4bd1ef6 100644 --- a/packages/client/lib/commands/RPUSHX.ts +++ b/packages/client/lib/commands/RPUSHX.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the RPUSHX command + * + * @param parser - The command parser + * @param key - The list key to push to (only if it exists) + * @param element - One or more elements to push + * @see https://redis.io/commands/rpushx/ + */ parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) { parser.push('RPUSHX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SADD.ts b/packages/client/lib/commands/SADD.ts index 1fb0171d8d4..3ee55706b95 100644 --- a/packages/client/lib/commands/SADD.ts +++ b/packages/client/lib/commands/SADD.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the SADD command + * + * @param parser - The command parser + * @param key - The set key to add members to + * @param members - One or more members to add to the set + * @see https://redis.io/commands/sadd/ + */ parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) { parser.push('SADD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts index ee78884083c..078b14da7a3 100644 --- a/packages/client/lib/commands/SAVE.ts +++ b/packages/client/lib/commands/SAVE.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SAVE command + * + * @param parser - The command parser + * @see https://redis.io/commands/save/ + */ parseCommand(parser: CommandParser) { parser.push('SAVE'); }, diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 2d6e4c35258..41991a24172 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -1,11 +1,24 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, CommandArguments, BlobStringReply, ArrayReply, Command } from '../RESP/types'; +/** + * Common options for SCAN-type commands + * + * @property MATCH - Pattern to filter returned keys + * @property COUNT - Hint for how many elements to return per iteration + */ export interface ScanCommonOptions { MATCH?: string; COUNT?: number; } +/** + * Parses scan arguments for SCAN-type commands + * + * @param parser - The command parser + * @param cursor - The cursor position for iteration + * @param options - Scan options + */ export function parseScanArguments( parser: CommandParser, cursor: RedisArgument, @@ -21,6 +34,14 @@ export function parseScanArguments( } } +/** + * Pushes scan arguments to the command arguments array + * + * @param args - The command arguments array + * @param cursor - The cursor position for iteration + * @param options - Scan options + * @returns The updated command arguments array + */ export function pushScanArguments( args: CommandArguments, cursor: RedisArgument, @@ -39,6 +60,11 @@ export function pushScanArguments( return args; } +/** + * Options for the SCAN command + * + * @property TYPE - Filter by value type + */ export interface ScanOptions extends ScanCommonOptions { TYPE?: RedisArgument; } @@ -46,6 +72,14 @@ export interface ScanOptions extends ScanCommonOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCAN command + * + * @param parser - The command parser + * @param cursor - The cursor position to start scanning from + * @param options - Scan options + * @see https://redis.io/commands/scan/ + */ parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) { parser.push('SCAN'); parseScanArguments(parser, cursor, options); @@ -54,6 +88,12 @@ export default { parser.push('TYPE', options.TYPE); } }, + /** + * Transforms the SCAN reply into a structured object + * + * @param reply - The raw reply containing cursor and keys + * @returns Object with cursor and keys properties + */ transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { return { cursor, diff --git a/packages/client/lib/commands/SCARD.ts b/packages/client/lib/commands/SCARD.ts index 61d4792d996..20a2aefae00 100644 --- a/packages/client/lib/commands/SCARD.ts +++ b/packages/client/lib/commands/SCARD.ts @@ -4,6 +4,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SCARD command + * + * @param parser - The command parser + * @param key - The set key to get the cardinality of + * @see https://redis.io/commands/scard/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SCARD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.ts b/packages/client/lib/commands/SCRIPT_DEBUG.ts index b0d3079068f..3f09c550449 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts @@ -4,6 +4,13 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCRIPT DEBUG command + * + * @param parser - The command parser + * @param mode - Debug mode: YES, SYNC, or NO + * @see https://redis.io/commands/script-debug/ + */ parseCommand(parser: CommandParser, mode: 'YES' | 'SYNC' | 'NO') { parser.push('SCRIPT', 'DEBUG', mode); }, diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.ts b/packages/client/lib/commands/SCRIPT_EXISTS.ts index b0f6cbe2275..66479654a0d 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts @@ -5,6 +5,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCRIPT EXISTS command + * + * @param parser - The command parser + * @param sha1 - One or more SHA1 digests of scripts + * @see https://redis.io/commands/script-exists/ + */ parseCommand(parser: CommandParser, sha1: RedisVariadicArgument) { parser.push('SCRIPT', 'EXISTS'); parser.pushVariadic(sha1); diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.ts b/packages/client/lib/commands/SCRIPT_FLUSH.ts index 1e05a619bad..91b61a4e59a 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts @@ -4,6 +4,13 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCRIPT FLUSH command + * + * @param parser - The command parser + * @param mode - Optional flush mode: ASYNC or SYNC + * @see https://redis.io/commands/script-flush/ + */ parseCommand(parser: CommandParser, mode?: 'ASYNC' | 'SYNC') { parser.push('SCRIPT', 'FLUSH'); diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts index 26953506235..ee2b2835cc1 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCRIPT KILL command + * + * @param parser - The command parser + * @see https://redis.io/commands/script-kill/ + */ parseCommand(parser: CommandParser) { parser.push('SCRIPT', 'KILL'); }, diff --git a/packages/client/lib/commands/SCRIPT_LOAD.ts b/packages/client/lib/commands/SCRIPT_LOAD.ts index 58f7c00dfcd..6e9acb388fc 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.ts @@ -4,6 +4,13 @@ import { BlobStringReply, Command, RedisArgument } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the SCRIPT LOAD command + * + * @param parser - The command parser + * @param script - The Lua script to load + * @see https://redis.io/commands/script-load/ + */ parseCommand(parser: CommandParser, script: RedisArgument) { parser.push('SCRIPT', 'LOAD', script); }, diff --git a/packages/client/lib/commands/SDIFF.ts b/packages/client/lib/commands/SDIFF.ts index bd78edc93db..07d700adac6 100644 --- a/packages/client/lib/commands/SDIFF.ts +++ b/packages/client/lib/commands/SDIFF.ts @@ -5,6 +5,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SDIFF command + * + * @param parser - The command parser + * @param keys - One or more set keys to compute the difference from + * @see https://redis.io/commands/sdiff/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SDIFF'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SDIFFSTORE.ts b/packages/client/lib/commands/SDIFFSTORE.ts index 6da2795d8ff..478d015d8c0 100644 --- a/packages/client/lib/commands/SDIFFSTORE.ts +++ b/packages/client/lib/commands/SDIFFSTORE.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + /** + * Constructs the SDIFFSTORE command + * + * @param parser - The command parser + * @param destination - The destination key to store the result + * @param keys - One or more set keys to compute the difference from + * @see https://redis.io/commands/sdiffstore/ + */ parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { parser.push('SDIFFSTORE'); parser.pushKey(destination); diff --git a/packages/client/lib/commands/SET.ts b/packages/client/lib/commands/SET.ts index d2d13c874c4..d1384255679 100644 --- a/packages/client/lib/commands/SET.ts +++ b/packages/client/lib/commands/SET.ts @@ -43,6 +43,15 @@ export interface SetOptions { } export default { + /** + * Constructs the SET command + * + * @param parser - The command parser + * @param key - The key to set + * @param value - The value to set + * @param options - Additional options for the SET command + * @see https://redis.io/commands/set/ + */ parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument | number, options?: SetOptions) { parser.push('SET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SETBIT.ts b/packages/client/lib/commands/SETBIT.ts index 5cd29260071..b9c29796db9 100644 --- a/packages/client/lib/commands/SETBIT.ts +++ b/packages/client/lib/commands/SETBIT.ts @@ -4,6 +4,15 @@ import { BitValue } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the SETBIT command + * + * @param parser - The command parser + * @param key - The key to set the bit on + * @param offset - The bit offset (zero-based) + * @param value - The bit value (0 or 1) + * @see https://redis.io/commands/setbit/ + */ parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: BitValue) { parser.push('SETBIT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SETEX.ts b/packages/client/lib/commands/SETEX.ts index 5e58b589975..39c7c60f53b 100644 --- a/packages/client/lib/commands/SETEX.ts +++ b/packages/client/lib/commands/SETEX.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { + /** + * Constructs the SETEX command + * + * @param parser - The command parser + * @param key - The key to set + * @param seconds - The expiration time in seconds + * @param value - The value to set + * @see https://redis.io/commands/setex/ + */ parseCommand(parser: CommandParser, key: RedisArgument, seconds: number, value: RedisArgument) { parser.push('SETEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SETNX.ts b/packages/client/lib/commands/SETNX.ts index ae60067c28f..b32b6c5ef34 100644 --- a/packages/client/lib/commands/SETNX.ts +++ b/packages/client/lib/commands/SETNX.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the SETNX command + * + * @param parser - The command parser + * @param key - The key to set if it doesn't exist + * @param value - The value to set + * @see https://redis.io/commands/setnx/ + */ parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { parser.push('SETNX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SETRANGE.ts b/packages/client/lib/commands/SETRANGE.ts index 42f4ca01117..366e6c28a7d 100644 --- a/packages/client/lib/commands/SETRANGE.ts +++ b/packages/client/lib/commands/SETRANGE.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { + /** + * Constructs the SETRANGE command + * + * @param parser - The command parser + * @param key - The key to modify + * @param offset - The offset at which to start writing + * @param value - The value to write at the offset + * @see https://redis.io/commands/setrange/ + */ parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: RedisArgument) { parser.push('SETRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts index 33fb3e77301..6a5416d430c 100644 --- a/packages/client/lib/commands/SHUTDOWN.ts +++ b/packages/client/lib/commands/SHUTDOWN.ts @@ -1,6 +1,14 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; +/** + * Options for the SHUTDOWN command + * + * @property mode - NOSAVE will not save DB, SAVE will force save DB + * @property NOW - Immediately terminate all clients + * @property FORCE - Force shutdown even in case of errors + * @property ABORT - Abort a shutdown in progress + */ export interface ShutdownOptions { mode?: 'NOSAVE' | 'SAVE'; NOW?: boolean; @@ -11,6 +19,13 @@ export interface ShutdownOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Constructs the SHUTDOWN command + * + * @param parser - The command parser + * @param options - Options for the shutdown process + * @see https://redis.io/commands/shutdown/ + */ parseCommand(parser: CommandParser, options?: ShutdownOptions) { parser.push('SHUTDOWN'); diff --git a/packages/client/lib/commands/SINTER.ts b/packages/client/lib/commands/SINTER.ts index 19ecdbb41ca..a129d71fd7a 100644 --- a/packages/client/lib/commands/SINTER.ts +++ b/packages/client/lib/commands/SINTER.ts @@ -5,6 +5,13 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SINTER command + * + * @param parser - The command parser + * @param keys - One or more set keys to compute the intersection from + * @see https://redis.io/commands/sinter/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SINTER'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SINTERCARD.ts b/packages/client/lib/commands/SINTERCARD.ts index cb9e7d3be3d..191c0881a8d 100644 --- a/packages/client/lib/commands/SINTERCARD.ts +++ b/packages/client/lib/commands/SINTERCARD.ts @@ -2,13 +2,25 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; +/** + * Options for the SINTERCARD command + * + * @property LIMIT - Maximum number of elements to return + */ export interface SInterCardOptions { LIMIT?: number; } export default { IS_READ_ONLY: true, - // option `number` for backwards compatibility + /** + * Constructs the SINTERCARD command + * + * @param parser - The command parser + * @param keys - One or more set keys to compute the intersection cardinality from + * @param options - Options for the SINTERCARD command or a number for LIMIT (backwards compatibility) + * @see https://redis.io/commands/sintercard/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SInterCardOptions | number) { parser.push('SINTERCARD'); parser.pushKeysLength(keys); diff --git a/packages/client/lib/commands/SINTERSTORE.ts b/packages/client/lib/commands/SINTERSTORE.ts index 06db0af9cb0..377b63fbddc 100644 --- a/packages/client/lib/commands/SINTERSTORE.ts +++ b/packages/client/lib/commands/SINTERSTORE.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the SINTERSTORE command + * + * @param parser - The command parser + * @param destination - The destination key to store the result + * @param keys - One or more set keys to compute the intersection from + * @see https://redis.io/commands/sinterstore/ + */ parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { parser.push('SINTERSTORE'); parser.pushKey(destination) diff --git a/packages/client/lib/commands/SISMEMBER.ts b/packages/client/lib/commands/SISMEMBER.ts index 6192ca2605f..3310d43d97b 100644 --- a/packages/client/lib/commands/SISMEMBER.ts +++ b/packages/client/lib/commands/SISMEMBER.ts @@ -4,6 +4,14 @@ import { NumberReply, Command, RedisArgument } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SISMEMBER command + * + * @param parser - The command parser + * @param key - The set key to check membership in + * @param member - The member to check for existence + * @see https://redis.io/commands/sismember/ + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('SISMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMEMBERS.ts b/packages/client/lib/commands/SMEMBERS.ts index 6d018e999f4..399ffd86147 100644 --- a/packages/client/lib/commands/SMEMBERS.ts +++ b/packages/client/lib/commands/SMEMBERS.ts @@ -4,6 +4,13 @@ import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '. export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SMEMBERS command + * + * @param parser - The command parser + * @param key - The set key to get all members from + * @see https://redis.io/commands/smembers/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SMEMBERS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMISMEMBER.ts b/packages/client/lib/commands/SMISMEMBER.ts index f0f3a143c7f..b5950dcfd7f 100644 --- a/packages/client/lib/commands/SMISMEMBER.ts +++ b/packages/client/lib/commands/SMISMEMBER.ts @@ -4,6 +4,14 @@ import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SMISMEMBER command + * + * @param parser - The command parser + * @param key - The set key to check membership in + * @param members - The members to check for existence + * @see https://redis.io/commands/smismember/ + */ parseCommand(parser: CommandParser, key: RedisArgument, members: Array) { parser.push('SMISMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMOVE.ts b/packages/client/lib/commands/SMOVE.ts index d87eeefdfbf..d5f150b99f2 100644 --- a/packages/client/lib/commands/SMOVE.ts +++ b/packages/client/lib/commands/SMOVE.ts @@ -3,6 +3,15 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: false, + /** + * Constructs the SMOVE command + * + * @param parser - The command parser + * @param source - The source set key + * @param destination - The destination set key + * @param member - The member to move + * @see https://redis.io/commands/smove/ + */ parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, member: RedisArgument) { parser.push('SMOVE'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/SORT.ts b/packages/client/lib/commands/SORT.ts index 3738d327d91..5ec889f3063 100644 --- a/packages/client/lib/commands/SORT.ts +++ b/packages/client/lib/commands/SORT.ts @@ -1,6 +1,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +/** + * Options for the SORT command + * + * @property BY - Pattern for external key to sort by + * @property LIMIT - Offset and count for results pagination + * @property GET - Pattern(s) for retrieving external keys + * @property DIRECTION - Sort direction: ASC (ascending) or DESC (descending) + * @property ALPHA - Sort lexicographically instead of numerically + */ export interface SortOptions { BY?: RedisArgument; LIMIT?: { @@ -12,6 +21,13 @@ export interface SortOptions { ALPHA?: boolean; } +/** + * Parses sort arguments for the SORT command + * + * @param parser - The command parser + * @param key - The key to sort + * @param options - Sort options + */ export function parseSortArguments( parser: CommandParser, key: RedisArgument, @@ -52,6 +68,14 @@ export function parseSortArguments( export default { IS_READ_ONLY: true, + /** + * Constructs the SORT command + * + * @param parser - The command parser + * @param key - The key to sort (list, set, or sorted set) + * @param options - Sort options + * @see https://redis.io/commands/sort/ + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: SortOptions) { parser.push('SORT'); parseSortArguments(parser, key, options); diff --git a/packages/client/lib/commands/SORT_RO.ts b/packages/client/lib/commands/SORT_RO.ts index 9901907c223..5531f927d52 100644 --- a/packages/client/lib/commands/SORT_RO.ts +++ b/packages/client/lib/commands/SORT_RO.ts @@ -3,6 +3,10 @@ import SORT, { parseSortArguments } from './SORT'; export default { IS_READ_ONLY: true, + /** + * Read-only variant of SORT that sorts the elements in a list, set or sorted set. + * @param args - Same parameters as the SORT command. + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/SORT_STORE.ts b/packages/client/lib/commands/SORT_STORE.ts index 15c94732e41..5fd52e076df 100644 --- a/packages/client/lib/commands/SORT_STORE.ts +++ b/packages/client/lib/commands/SORT_STORE.ts @@ -4,6 +4,13 @@ import SORT, { SortOptions } from './SORT'; export default { IS_READ_ONLY: false, + /** + * Sorts the elements in a list, set or sorted set and stores the result in a new list. + * @param parser - The Redis command parser. + * @param source - Key of the source list, set or sorted set. + * @param destination - Destination key where the result will be stored. + * @param options - Optional sorting parameters. + */ parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: SortOptions) { SORT.parseCommand(parser, source, options); parser.push('STORE', destination); diff --git a/packages/client/lib/commands/SPOP.ts b/packages/client/lib/commands/SPOP.ts index 38f40989e63..8e9450b2b01 100644 --- a/packages/client/lib/commands/SPOP.ts +++ b/packages/client/lib/commands/SPOP.ts @@ -3,6 +3,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: false, + /** + * Constructs the SPOP command to remove and return a random member from a set + * + * @param parser - The command parser + * @param key - The key of the set to pop from + * @see https://redis.io/commands/spop/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts index 0536203be97..1191f07cff2 100644 --- a/packages/client/lib/commands/SPOP_COUNT.ts +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -3,6 +3,14 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: false, + /** + * Constructs the SPOP command to remove and return multiple random members from a set + * + * @param parser - The command parser + * @param key - The key of the set to pop from + * @param count - The number of members to pop + * @see https://redis.io/commands/spop/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('SPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SPUBLISH.ts b/packages/client/lib/commands/SPUBLISH.ts index 77d93e617de..6dd9f37e66b 100644 --- a/packages/client/lib/commands/SPUBLISH.ts +++ b/packages/client/lib/commands/SPUBLISH.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the SPUBLISH command to post a message to a Sharded Pub/Sub channel + * + * @param parser - The command parser + * @param channel - The channel to publish to + * @param message - The message to publish + * @see https://redis.io/commands/spublish/ + */ parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { parser.push('SPUBLISH'); parser.pushKey(channel); diff --git a/packages/client/lib/commands/SRANDMEMBER.ts b/packages/client/lib/commands/SRANDMEMBER.ts index 4285f7aa17c..9e04e45b52a 100644 --- a/packages/client/lib/commands/SRANDMEMBER.ts +++ b/packages/client/lib/commands/SRANDMEMBER.ts @@ -3,6 +3,13 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Constructs the SRANDMEMBER command to get a random member from a set + * + * @param parser - The command parser + * @param key - The key of the set to get random member from + * @see https://redis.io/commands/srandmember/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SRANDMEMBER') parser.pushKey(key); diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts index dd72245c3b3..c7dd434b710 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts @@ -4,6 +4,14 @@ import SRANDMEMBER from './SRANDMEMBER'; export default { IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY, + /** + * Constructs the SRANDMEMBER command to get multiple random members from a set + * + * @param parser - The command parser + * @param key - The key of the set to get random members from + * @param count - The number of members to return. If negative, may return the same member multiple times + * @see https://redis.io/commands/srandmember/ + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { SRANDMEMBER.parseCommand(parser, key); parser.push(count.toString()); diff --git a/packages/client/lib/commands/SREM.ts b/packages/client/lib/commands/SREM.ts index 75053474cce..d97ed7774d8 100644 --- a/packages/client/lib/commands/SREM.ts +++ b/packages/client/lib/commands/SREM.ts @@ -4,6 +4,15 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the SREM command to remove one or more members from a set + * + * @param parser - The command parser + * @param key - The key of the set to remove members from + * @param members - One or more members to remove from the set + * @returns The number of members that were removed from the set + * @see https://redis.io/commands/srem/ + */ parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) { parser.push('SREM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SSCAN.ts b/packages/client/lib/commands/SSCAN.ts index 22634d56242..14e2c079ff0 100644 --- a/packages/client/lib/commands/SSCAN.ts +++ b/packages/client/lib/commands/SSCAN.ts @@ -4,6 +4,16 @@ import { ScanCommonOptions, parseScanArguments} from './SCAN'; export default { IS_READ_ONLY: true, + /** + * Constructs the SSCAN command to incrementally iterate over elements in a set + * + * @param parser - The command parser + * @param key - The key of the set to scan + * @param cursor - The cursor position to start scanning from + * @param options - Optional scanning parameters (COUNT and MATCH) + * @returns Iterator containing cursor position and matching members + * @see https://redis.io/commands/sscan/ + */ parseCommand( parser: CommandParser, key: RedisArgument, @@ -14,6 +24,13 @@ export default { parser.pushKey(key); parseScanArguments(parser, cursor, options); }, + /** + * Transforms the SSCAN reply into a cursor result object + * + * @param cursor - The next cursor position + * @param members - Array of matching set members + * @returns Object containing cursor and members array + */ transformReply([cursor, members]: [BlobStringReply, Array]) { return { cursor, diff --git a/packages/client/lib/commands/STRLEN.ts b/packages/client/lib/commands/STRLEN.ts index 34e0430fc9e..0f0e612422a 100644 --- a/packages/client/lib/commands/STRLEN.ts +++ b/packages/client/lib/commands/STRLEN.ts @@ -4,6 +4,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the STRLEN command to get the length of a string value + * + * @param parser - The command parser + * @param key - The key holding the string value + * @returns The length of the string value, or 0 when key does not exist + * @see https://redis.io/commands/strlen/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('STRLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SUNION.ts b/packages/client/lib/commands/SUNION.ts index 3d9a5954a7c..7acecd1d12a 100644 --- a/packages/client/lib/commands/SUNION.ts +++ b/packages/client/lib/commands/SUNION.ts @@ -5,6 +5,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the SUNION command to return the members of the set resulting from the union of all the given sets + * + * @param parser - The command parser + * @param keys - One or more set keys to compute the union from + * @returns Array of all elements that are members of at least one of the given sets + * @see https://redis.io/commands/sunion/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SUNION'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SUNIONSTORE.ts b/packages/client/lib/commands/SUNIONSTORE.ts index e2f43ecb1c8..0a877c9cb8d 100644 --- a/packages/client/lib/commands/SUNIONSTORE.ts +++ b/packages/client/lib/commands/SUNIONSTORE.ts @@ -4,6 +4,15 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the SUNIONSTORE command to store the union of multiple sets into a destination set + * + * @param parser - The command parser + * @param destination - The destination key to store the resulting set + * @param keys - One or more source set keys to compute the union from + * @returns The number of elements in the resulting set + * @see https://redis.io/commands/sunionstore/ + */ parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { parser.push('SUNIONSTORE'); parser.pushKey(destination); diff --git a/packages/client/lib/commands/SWAPDB.ts b/packages/client/lib/commands/SWAPDB.ts index e59c75715cd..66b19409a2b 100644 --- a/packages/client/lib/commands/SWAPDB.ts +++ b/packages/client/lib/commands/SWAPDB.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Swaps the data of two Redis databases. + * @param parser - The Redis command parser. + * @param index1 - First database index. + * @param index2 - Second database index. + */ parseCommand(parser: CommandParser, index1: number, index2: number) { parser.push('SWAPDB', index1.toString(), index2.toString()); }, diff --git a/packages/client/lib/commands/TIME.ts b/packages/client/lib/commands/TIME.ts index b25af710e1c..dc248d82069 100644 --- a/packages/client/lib/commands/TIME.ts +++ b/packages/client/lib/commands/TIME.ts @@ -4,6 +4,13 @@ import { BlobStringReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the TIME command to return the server's current time + * + * @param parser - The command parser + * @returns Array containing the Unix timestamp in seconds and microseconds + * @see https://redis.io/commands/time/ + */ parseCommand(parser: CommandParser) { parser.push('TIME'); }, diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts index c765c9f8347..953a696111c 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the TOUCH command to alter the last access time of keys + * + * @param parser - The command parser + * @param key - One or more keys to touch + * @returns The number of keys that were touched + * @see https://redis.io/commands/touch/ + */ parseCommand(parser: CommandParser, key: RedisVariadicArgument) { parser.push('TOUCH'); parser.pushKeys(key); diff --git a/packages/client/lib/commands/TTL.ts b/packages/client/lib/commands/TTL.ts index 8420089fcb9..c3340eda32e 100644 --- a/packages/client/lib/commands/TTL.ts +++ b/packages/client/lib/commands/TTL.ts @@ -3,6 +3,14 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: true, + /** + * Constructs the TTL command to get the remaining time to live of a key + * + * @param parser - The command parser + * @param key - Key to check + * @returns Time to live in seconds, -2 if key does not exist, -1 if has no timeout + * @see https://redis.io/commands/ttl/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TTL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/TYPE.ts b/packages/client/lib/commands/TYPE.ts index ffc592994db..740aa08e94a 100644 --- a/packages/client/lib/commands/TYPE.ts +++ b/packages/client/lib/commands/TYPE.ts @@ -4,6 +4,14 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the TYPE command to determine the data type stored at key + * + * @param parser - The command parser + * @param key - Key to check + * @returns String reply: "none", "string", "list", "set", "zset", "hash", "stream" + * @see https://redis.io/commands/type/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TYPE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/UNLINK.ts b/packages/client/lib/commands/UNLINK.ts index 14d1e700277..4aa9cc315ab 100644 --- a/packages/client/lib/commands/UNLINK.ts +++ b/packages/client/lib/commands/UNLINK.ts @@ -4,6 +4,14 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the UNLINK command to asynchronously delete one or more keys + * + * @param parser - The command parser + * @param keys - One or more keys to unlink + * @returns The number of keys that were unlinked + * @see https://redis.io/commands/unlink/ + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('UNLINK'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/WAIT.ts b/packages/client/lib/commands/WAIT.ts index df45a12373d..7ccebbc4ec9 100644 --- a/packages/client/lib/commands/WAIT.ts +++ b/packages/client/lib/commands/WAIT.ts @@ -4,6 +4,15 @@ import { NumberReply, Command } from '../RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Constructs the WAIT command to synchronize with replicas + * + * @param parser - The command parser + * @param numberOfReplicas - Number of replicas that must acknowledge the write + * @param timeout - Maximum time to wait in milliseconds + * @returns The number of replicas that acknowledged the write + * @see https://redis.io/commands/wait/ + */ parseCommand(parser: CommandParser, numberOfReplicas: number, timeout: number) { parser.push('WAIT', numberOfReplicas.toString(), timeout.toString()); }, diff --git a/packages/client/lib/commands/XACK.ts b/packages/client/lib/commands/XACK.ts index 2500134f1c8..26e1c962baa 100644 --- a/packages/client/lib/commands/XACK.ts +++ b/packages/client/lib/commands/XACK.ts @@ -4,6 +4,16 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Constructs the XACK command to acknowledge the processing of stream messages in a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - The consumer group name + * @param id - One or more message IDs to acknowledge + * @returns The number of messages successfully acknowledged + * @see https://redis.io/commands/xack/ + */ parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisVariadicArgument) { parser.push('XACK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index cb9d0f5fad8..b0c50b1bfdb 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; import { Tail } from './generic-transformers'; +/** + * Options for the XADD command + * + * @property TRIM - Optional trimming configuration + * @property TRIM.strategy - Trim strategy: MAXLEN (by length) or MINID (by ID) + * @property TRIM.strategyModifier - Exact ('=') or approximate ('~') trimming + * @property TRIM.threshold - Maximum stream length or minimum ID to retain + * @property TRIM.limit - Maximum number of entries to trim in one call + */ export interface XAddOptions { TRIM?: { strategy?: 'MAXLEN' | 'MINID'; @@ -11,6 +20,16 @@ export interface XAddOptions { }; } +/** + * Parses arguments for the XADD command + * + * @param optional - Optional command modifier + * @param parser - The command parser + * @param key - The stream key + * @param id - Message ID (* for auto-generation) + * @param message - Key-value pairs representing the message fields + * @param options - Additional options for stream trimming + */ export function parseXAddArguments( optional: RedisArgument | undefined, parser: CommandParser, @@ -50,6 +69,17 @@ export function parseXAddArguments( export default { IS_READ_ONLY: false, + /** + * Constructs the XADD command to append a new entry to a stream + * + * @param parser - The command parser + * @param key - The stream key + * @param id - Message ID (* for auto-generation) + * @param message - Key-value pairs representing the message fields + * @param options - Additional options for stream trimming + * @returns The ID of the added entry + * @see https://redis.io/commands/xadd/ + */ parseCommand(...args: Tail>) { return parseXAddArguments(undefined, ...args); }, diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.ts index 9d33374be4a..8b1861a065b 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts @@ -2,8 +2,18 @@ import { BlobStringReply, NullReply, Command } from '../RESP/types'; import { Tail } from './generic-transformers'; import { parseXAddArguments } from './XADD'; +/** + * Command for adding entries to an existing stream without creating it if it doesn't exist + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XADD command with NOMKSTREAM option to append a new entry to an existing stream + * + * @param args - Arguments tuple containing parser, key, id, message, and options + * @returns The ID of the added entry, or null if the stream doesn't exist + * @see https://redis.io/commands/xadd/ + */ parseCommand(...args: Tail>) { return parseXAddArguments('NOMKSTREAM', ...args); }, diff --git a/packages/client/lib/commands/XAUTOCLAIM.ts b/packages/client/lib/commands/XAUTOCLAIM.ts index 19b4f63a2df..bd6f7b05346 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.ts @@ -2,10 +2,22 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesReply, BlobStringReply, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; +/** + * Options for the XAUTOCLAIM command + * + * @property COUNT - Limit the number of messages to claim + */ export interface XAutoClaimOptions { COUNT?: number; } +/** + * Raw reply structure for XAUTOCLAIM command + * + * @property nextId - The ID to use for the next XAUTOCLAIM call + * @property messages - Array of claimed messages or null entries + * @property deletedMessages - Array of message IDs that no longer exist + */ export type XAutoClaimRawReply = TuplesReply<[ nextId: BlobStringReply, messages: ArrayReply, @@ -14,6 +26,19 @@ export type XAutoClaimRawReply = TuplesReply<[ export default { IS_READ_ONLY: false, + /** + * Constructs the XAUTOCLAIM command to automatically claim pending messages in a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - The consumer group name + * @param consumer - The consumer name that will claim the messages + * @param minIdleTime - Minimum idle time in milliseconds for a message to be claimed + * @param start - Message ID to start scanning from + * @param options - Additional options for the claim operation + * @returns Object containing nextId, claimed messages, and list of deleted message IDs + * @see https://redis.io/commands/xautoclaim/ + */ parseCommand( parser: CommandParser, key: RedisArgument, @@ -31,6 +56,14 @@ export default { parser.push('COUNT', options.COUNT.toString()); } }, + /** + * Transforms the raw XAUTOCLAIM reply into a structured object + * + * @param reply - Raw reply from Redis + * @param preserve - Preserve options (unused) + * @param typeMapping - Type mapping for message fields + * @returns Structured object containing nextId, messages, and deletedMessages + */ transformReply(reply: UnwrapReply, preserve?: any, typeMapping?: TypeMapping) { return { nextId: reply[0], diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts index c0ebe83748e..efa299c6f8f 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -1,6 +1,13 @@ import { TuplesReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; import XAUTOCLAIM from './XAUTOCLAIM'; +/** + * Raw reply structure for XAUTOCLAIM JUSTID command + * + * @property nextId - The ID to use for the next XAUTOCLAIM call + * @property messages - Array of message IDs that were claimed + * @property deletedMessages - Array of message IDs that no longer exist + */ type XAutoClaimJustIdRawReply = TuplesReply<[ nextId: BlobStringReply, messages: ArrayReply, @@ -9,11 +16,24 @@ type XAutoClaimJustIdRawReply = TuplesReply<[ export default { IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY, + /** + * Constructs the XAUTOCLAIM command with JUSTID option to get only message IDs + * + * @param args - Same parameters as XAUTOCLAIM command + * @returns Object containing nextId and arrays of claimed and deleted message IDs + * @see https://redis.io/commands/xautoclaim/ + */ parseCommand(...args: Parameters) { const parser = args[0]; XAUTOCLAIM.parseCommand(...args); parser.push('JUSTID'); }, + /** + * Transforms the raw XAUTOCLAIM JUSTID reply into a structured object + * + * @param reply - Raw reply from Redis + * @returns Structured object containing nextId, message IDs, and deleted message IDs + */ transformReply(reply: UnwrapReply) { return { nextId: reply[0], diff --git a/packages/client/lib/commands/XCLAIM.ts b/packages/client/lib/commands/XCLAIM.ts index 598b1b17ba4..2bc771288ac 100644 --- a/packages/client/lib/commands/XCLAIM.ts +++ b/packages/client/lib/commands/XCLAIM.ts @@ -2,6 +2,15 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { RedisVariadicArgument, StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; +/** + * Options for the XCLAIM command + * + * @property IDLE - Set the idle time (in milliseconds) for the claimed messages + * @property TIME - Set the last delivery time (Unix timestamp or Date) + * @property RETRYCOUNT - Set the retry counter for the claimed messages + * @property FORCE - Create the pending message entry even if the message doesn't exist + * @property LASTID - Update the consumer group last ID + */ export interface XClaimOptions { IDLE?: number; TIME?: number | Date; @@ -12,6 +21,19 @@ export interface XClaimOptions { export default { IS_READ_ONLY: false, + /** + * Constructs the XCLAIM command to claim pending messages in a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - The consumer group name + * @param consumer - The consumer name that will claim the messages + * @param minIdleTime - Minimum idle time in milliseconds for a message to be claimed + * @param id - One or more message IDs to claim + * @param options - Additional options for the claim operation + * @returns Array of claimed messages + * @see https://redis.io/commands/xclaim/ + */ parseCommand( parser: CommandParser, key: RedisArgument, @@ -49,6 +71,14 @@ export default { parser.push('LASTID', options.LASTID); } }, + /** + * Transforms the raw XCLAIM reply into an array of messages + * + * @param reply - Raw reply from Redis + * @param preserve - Preserve options (unused) + * @param typeMapping - Type mapping for message fields + * @returns Array of claimed messages with their fields + */ transformReply( reply: UnwrapReply>, preserve?: any, diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.ts b/packages/client/lib/commands/XCLAIM_JUSTID.ts index 91be5aafbb4..56e1d576158 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts @@ -1,12 +1,27 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import XCLAIM from './XCLAIM'; +/** + * Command variant for XCLAIM that returns only message IDs + */ export default { IS_READ_ONLY: XCLAIM.IS_READ_ONLY, + /** + * Constructs the XCLAIM command with JUSTID option to get only message IDs + * + * @param args - Same parameters as XCLAIM command + * @returns Array of successfully claimed message IDs + * @see https://redis.io/commands/xclaim/ + */ parseCommand(...args: Parameters) { const parser = args[0]; XCLAIM.parseCommand(...args); parser.push('JUSTID'); }, + /** + * Transforms the XCLAIM JUSTID reply into an array of message IDs + * + * @returns Array of claimed message IDs + */ transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XDEL.ts b/packages/client/lib/commands/XDEL.ts index ee385203ce5..db8df7d4fd2 100644 --- a/packages/client/lib/commands/XDEL.ts +++ b/packages/client/lib/commands/XDEL.ts @@ -2,8 +2,20 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; +/** + * Command for removing messages from a stream + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XDEL command to remove one or more messages from a stream + * + * @param parser - The command parser + * @param key - The stream key + * @param id - One or more message IDs to delete + * @returns The number of messages actually deleted + * @see https://redis.io/commands/xdel/ + */ parseCommand(parser: CommandParser, key: RedisArgument, id: RedisVariadicArgument) { parser.push('XDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XGROUP_CREATE.ts b/packages/client/lib/commands/XGROUP_CREATE.ts index e91186efe29..db6df04fa0f 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.ts @@ -1,6 +1,12 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +/** + * Options for creating a consumer group + * + * @property MKSTREAM - Create the stream if it doesn't exist + * @property ENTRIESREAD - Set the number of entries that were read in this consumer group (Redis 7.0+) + */ export interface XGroupCreateOptions { MKSTREAM?: boolean; /** @@ -11,6 +17,17 @@ export interface XGroupCreateOptions { export default { IS_READ_ONLY: false, + /** + * Constructs the XGROUP CREATE command to create a consumer group for a stream + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group + * @param id - ID of the last delivered item in the stream ('$' for last item, '0' for all items) + * @param options - Additional options for group creation + * @returns 'OK' if successful + * @see https://redis.io/commands/xgroup-create/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts index 906bc4c683e..0b730c7f96b 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts @@ -1,8 +1,21 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, Command, NumberReply } from '../RESP/types'; +/** + * Command for creating a new consumer in a consumer group + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XGROUP CREATECONSUMER command to create a new consumer in a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group + * @param consumer - Name of the consumer to create + * @returns 1 if the consumer was created, 0 if it already existed + * @see https://redis.io/commands/xgroup-createconsumer/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts index 360d7e06cae..5feffe74042 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts @@ -1,8 +1,21 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; +/** + * Command for removing a consumer from a consumer group + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XGROUP DELCONSUMER command to remove a consumer from a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group + * @param consumer - Name of the consumer to remove + * @returns The number of pending messages owned by the deleted consumer + * @see https://redis.io/commands/xgroup-delconsumer/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_DESTROY.ts b/packages/client/lib/commands/XGROUP_DESTROY.ts index 9112f1bcd79..ed454abbb2b 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.ts @@ -1,8 +1,20 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; +/** + * Command for removing a consumer group + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XGROUP DESTROY command to remove a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group to destroy + * @returns 1 if the group was destroyed, 0 if it did not exist + * @see https://redis.io/commands/xgroup-destroy/ + */ parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { parser.push('XGROUP', 'DESTROY'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XGROUP_SETID.ts b/packages/client/lib/commands/XGROUP_SETID.ts index 5b0ddcc32ad..4f3076b6032 100644 --- a/packages/client/lib/commands/XGROUP_SETID.ts +++ b/packages/client/lib/commands/XGROUP_SETID.ts @@ -1,6 +1,11 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +/** + * Options for setting a consumer group's ID position + * + * @property ENTRIESREAD - Set the number of entries that were read in this consumer group (Redis 7.0+) + */ export interface XGroupSetIdOptions { /** added in 7.0 */ ENTRIESREAD?: number; @@ -8,6 +13,17 @@ export interface XGroupSetIdOptions { export default { IS_READ_ONLY: false, + /** + * Constructs the XGROUP SETID command to set the last delivered ID for a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group + * @param id - ID to set as last delivered message ('$' for last item, '0' for all items) + * @param options - Additional options for setting the group ID + * @returns 'OK' if successful + * @see https://redis.io/commands/xgroup-setid/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.ts b/packages/client/lib/commands/XINFO_CONSUMERS.ts index 310a40d17f3..49267f13980 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts @@ -1,6 +1,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +/** + * Reply structure for XINFO CONSUMERS command + * + * @property name - Name of the consumer + * @property pending - Number of pending messages for this consumer + * @property idle - Idle time in milliseconds + * @property inactive - Time in milliseconds since last interaction (Redis 7.2+) + */ export type XInfoConsumersReply = ArrayReply, BlobStringReply], [BlobStringReply<'pending'>, NumberReply], @@ -11,12 +19,27 @@ export type XInfoConsumersReply = ArrayReply>) => { return reply.map(consumer => { const unwrapped = consumer as unknown as UnwrapReply; diff --git a/packages/client/lib/commands/XINFO_GROUPS.ts b/packages/client/lib/commands/XINFO_GROUPS.ts index e7f8874125a..1d8142bfaef 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.ts @@ -1,6 +1,9 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +/** + * Reply structure for XINFO GROUPS command containing information about consumer groups + */ export type XInfoGroupsReply = ArrayReply, BlobStringReply], [BlobStringReply<'consumers'>, NumberReply], @@ -14,11 +17,31 @@ export type XInfoGroupsReply = ArrayReply>) => { return reply.map(group => { const unwrapped = group as unknown as UnwrapReply; diff --git a/packages/client/lib/commands/XINFO_STREAM.ts b/packages/client/lib/commands/XINFO_STREAM.ts index bb102c591bb..546dd70cab7 100644 --- a/packages/client/lib/commands/XINFO_STREAM.ts +++ b/packages/client/lib/commands/XINFO_STREAM.ts @@ -2,6 +2,20 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, TuplesReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; import { isNullReply, transformTuplesReply } from './generic-transformers'; +/** + * Reply structure for XINFO STREAM command containing detailed information about a stream + * + * @property length - Number of entries in the stream + * @property radix-tree-keys - Number of radix tree keys + * @property radix-tree-nodes - Number of radix tree nodes + * @property last-generated-id - Last generated message ID + * @property max-deleted-entry-id - Highest message ID deleted (Redis 7.2+) + * @property entries-added - Total number of entries added (Redis 7.2+) + * @property recorded-first-entry-id - ID of the first recorded entry (Redis 7.2+) + * @property groups - Number of consumer groups + * @property first-entry - First entry in the stream + * @property last-entry - Last entry in the stream + */ export type XInfoStreamReply = TuplesToMapReply<[ [BlobStringReply<'length'>, NumberReply], [BlobStringReply<'radix-tree-keys'>, NumberReply], @@ -20,6 +34,14 @@ export type XInfoStreamReply = TuplesToMapReply<[ export default { IS_READ_ONLY: true, + /** + * Constructs the XINFO STREAM command to get detailed information about a stream + * + * @param parser - The command parser + * @param key - The stream key + * @returns Detailed information about the stream including its length, structure, and entries + * @see https://redis.io/commands/xinfo-stream/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('XINFO', 'STREAM'); parser.pushKey(key); @@ -67,11 +89,20 @@ export default { } } as const satisfies Command; +/** + * Raw entry structure from Redis stream + */ type RawEntry = TuplesReply<[ id: BlobStringReply, message: ArrayReply ]> | NullReply; +/** + * Transforms a raw stream entry into a structured object + * + * @param entry - Raw entry from Redis + * @returns Structured object with id and message, or null if entry is null + */ function transformEntry(entry: RawEntry) { if (isNullReply(entry)) return entry; diff --git a/packages/client/lib/commands/XLEN.ts b/packages/client/lib/commands/XLEN.ts index 39d47187b28..f7718371cf2 100644 --- a/packages/client/lib/commands/XLEN.ts +++ b/packages/client/lib/commands/XLEN.ts @@ -1,9 +1,20 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; +/** + * Command for getting the length of a stream + */ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the XLEN command to get the number of entries in a stream + * + * @param parser - The command parser + * @param key - The stream key + * @returns The number of entries inside the stream + * @see https://redis.io/commands/xlen/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('XLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XPENDING.ts b/packages/client/lib/commands/XPENDING.ts index 11c944c61e7..cff9ef2f51b 100644 --- a/packages/client/lib/commands/XPENDING.ts +++ b/packages/client/lib/commands/XPENDING.ts @@ -1,6 +1,14 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +/** + * Raw reply structure for XPENDING command + * + * @property pending - Number of pending messages in the group + * @property firstId - ID of the first pending message + * @property lastId - ID of the last pending message + * @property consumers - Array of consumer info with delivery counts + */ type XPendingRawReply = TuplesReply<[ pending: NumberReply, firstId: BlobStringReply | NullReply, @@ -14,11 +22,26 @@ type XPendingRawReply = TuplesReply<[ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the XPENDING command to inspect pending messages of a consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - Name of the consumer group + * @returns Summary of pending messages including total count, ID range, and per-consumer stats + * @see https://redis.io/commands/xpending/ + */ parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { parser.push('XPENDING'); parser.pushKey(key); parser.push(group); }, + /** + * Transforms the raw XPENDING reply into a structured object + * + * @param reply - Raw reply from Redis + * @returns Object containing pending count, ID range, and consumer statistics + */ transformReply(reply: UnwrapReply) { const consumers = reply[3] as unknown as UnwrapReply; return { diff --git a/packages/client/lib/commands/XPENDING_RANGE.ts b/packages/client/lib/commands/XPENDING_RANGE.ts index 8d98ffe7f1e..e136061fe9e 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.ts @@ -1,11 +1,25 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +/** + * Options for the XPENDING RANGE command + * + * @property IDLE - Filter by message idle time in milliseconds + * @property consumer - Filter by specific consumer name + */ export interface XPendingRangeOptions { IDLE?: number; consumer?: RedisArgument; } +/** + * Raw reply structure for XPENDING RANGE command + * + * @property id - Message ID + * @property consumer - Name of the consumer that holds the message + * @property millisecondsSinceLastDelivery - Time since last delivery attempt + * @property deliveriesCounter - Number of times this message was delivered + */ type XPendingRangeRawReply = ArrayReply) { return reply.map(pending => { const unwrapped = pending as unknown as UnwrapReply; diff --git a/packages/client/lib/commands/XRANGE.ts b/packages/client/lib/commands/XRANGE.ts index de6bb6c9b1b..4b83a66e5e6 100644 --- a/packages/client/lib/commands/XRANGE.ts +++ b/packages/client/lib/commands/XRANGE.ts @@ -2,10 +2,23 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { StreamMessageRawReply, transformStreamMessageReply } from './generic-transformers'; +/** + * Options for the XRANGE command + * + * @property COUNT - Limit the number of entries returned + */ export interface XRangeOptions { COUNT?: number; } +/** + * Helper function to build XRANGE command arguments + * + * @param start - Start of ID range (use '-' for minimum ID) + * @param end - End of ID range (use '+' for maximum ID) + * @param options - Additional options for the range query + * @returns Array of arguments for the XRANGE command + */ export function xRangeArguments( start: RedisArgument, end: RedisArgument, @@ -23,11 +36,28 @@ export function xRangeArguments( export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the XRANGE command to read stream entries in a specific range + * + * @param parser - The command parser + * @param key - The stream key + * @param args - Arguments tuple containing start ID, end ID, and options + * @returns Array of messages in the specified range + * @see https://redis.io/commands/xrange/ + */ parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { parser.push('XRANGE'); parser.pushKey(key); parser.pushVariadic(xRangeArguments(args[0], args[1], args[2])); }, + /** + * Transforms the raw XRANGE reply into structured message objects + * + * @param reply - Raw reply from Redis + * @param preserve - Preserve options (unused) + * @param typeMapping - Type mapping for message fields + * @returns Array of structured message objects + */ transformReply( reply: UnwrapReply>, preserve?: any, diff --git a/packages/client/lib/commands/XREAD.ts b/packages/client/lib/commands/XREAD.ts index b57fb8f3983..110443ad3a5 100644 --- a/packages/client/lib/commands/XREAD.ts +++ b/packages/client/lib/commands/XREAD.ts @@ -2,6 +2,12 @@ import { CommandParser } from '../client/parser'; import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; +/** + * Structure representing a stream to read from + * + * @property key - The stream key + * @property id - The message ID to start reading from + */ export interface XReadStream { key: RedisArgument; id: RedisArgument; @@ -9,6 +15,12 @@ export interface XReadStream { export type XReadStreams = Array | XReadStream; +/** + * Helper function to push stream keys and IDs to the command parser + * + * @param parser - The command parser + * @param streams - Single stream or array of streams to read from + */ export function pushXReadStreams(parser: CommandParser, streams: XReadStreams) { parser.push('STREAMS'); @@ -25,6 +37,12 @@ export function pushXReadStreams(parser: CommandParser, streams: XReadStreams) { } } +/** + * Options for the XREAD command + * + * @property COUNT - Limit the number of entries returned per stream + * @property BLOCK - Milliseconds to block waiting for new entries (0 for indefinite) + */ export interface XReadOptions { COUNT?: number; BLOCK?: number; @@ -32,6 +50,15 @@ export interface XReadOptions { export default { IS_READ_ONLY: true, + /** + * Constructs the XREAD command to read messages from one or more streams + * + * @param parser - The command parser + * @param streams - Single stream or array of streams to read from + * @param options - Additional options for reading streams + * @returns Array of stream entries, each containing the stream name and its messages + * @see https://redis.io/commands/xread/ + */ parseCommand(parser: CommandParser, streams: XReadStreams, options?: XReadOptions) { parser.push('XREAD'); @@ -45,6 +72,9 @@ export default { pushXReadStreams(parser, streams); }, + /** + * Transform functions for different RESP versions + */ transformReply: { 2: transformStreamsMessagesReplyResp2, 3: undefined as unknown as () => ReplyUnion diff --git a/packages/client/lib/commands/XREADGROUP.ts b/packages/client/lib/commands/XREADGROUP.ts index d0947e19a08..b274aab95fe 100644 --- a/packages/client/lib/commands/XREADGROUP.ts +++ b/packages/client/lib/commands/XREADGROUP.ts @@ -3,6 +3,13 @@ import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; import { XReadStreams, pushXReadStreams } from './XREAD'; import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; +/** + * Options for the XREADGROUP command + * + * @property COUNT - Limit the number of entries returned per stream + * @property BLOCK - Milliseconds to block waiting for new entries (0 for indefinite) + * @property NOACK - Skip adding the message to the PEL (Pending Entries List) + */ export interface XReadGroupOptions { COUNT?: number; BLOCK?: number; @@ -11,6 +18,17 @@ export interface XReadGroupOptions { export default { IS_READ_ONLY: true, + /** + * Constructs the XREADGROUP command to read messages from streams as a consumer group member + * + * @param parser - The command parser + * @param group - Name of the consumer group + * @param consumer - Name of the consumer in the group + * @param streams - Single stream or array of streams to read from + * @param options - Additional options for reading streams + * @returns Array of stream entries, each containing the stream name and its messages + * @see https://redis.io/commands/xreadgroup/ + */ parseCommand( parser: CommandParser, group: RedisArgument, @@ -34,6 +52,9 @@ export default { pushXReadStreams(parser, streams); }, + /** + * Transform functions for different RESP versions + */ transformReply: { 2: transformStreamsMessagesReplyResp2, 3: undefined as unknown as () => ReplyUnion diff --git a/packages/client/lib/commands/XREVRANGE.ts b/packages/client/lib/commands/XREVRANGE.ts index ddc51082a1e..452c2ab3807 100644 --- a/packages/client/lib/commands/XREVRANGE.ts +++ b/packages/client/lib/commands/XREVRANGE.ts @@ -2,13 +2,30 @@ import { CommandParser } from '../client/parser'; import { Command, RedisArgument } from '../RESP/types'; import XRANGE, { xRangeArguments } from './XRANGE'; +/** + * Options for the XREVRANGE command + * + * @property COUNT - Limit the number of entries returned + */ export interface XRevRangeOptions { COUNT?: number; } +/** + * Command for reading stream entries in reverse order + */ export default { CACHEABLE: XRANGE.CACHEABLE, IS_READ_ONLY: XRANGE.IS_READ_ONLY, + /** + * Constructs the XREVRANGE command to read stream entries in reverse order + * + * @param parser - The command parser + * @param key - The stream key + * @param args - Arguments tuple containing start ID, end ID, and options + * @returns Array of messages in the specified range in reverse order + * @see https://redis.io/commands/xrevrange/ + */ parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { parser.push('XREVRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index fb617d8d35a..6125720111a 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,14 +1,34 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; +/** + * Options for the XTRIM command + * + * @property strategyModifier - Exact ('=') or approximate ('~') trimming + * @property LIMIT - Maximum number of entries to trim in one call (Redis 6.2+) + */ export interface XTrimOptions { strategyModifier?: '=' | '~'; /** added in 6.2 */ LIMIT?: number; } +/** + * Command for trimming a stream to a specified length or minimum ID + */ export default { IS_READ_ONLY: false, + /** + * Constructs the XTRIM command to trim a stream by length or minimum ID + * + * @param parser - The command parser + * @param key - The stream key + * @param strategy - Trim by maximum length (MAXLEN) or minimum ID (MINID) + * @param threshold - Maximum length or minimum ID threshold + * @param options - Additional options for trimming + * @returns Number of entries removed from the stream + * @see https://redis.io/commands/xtrim/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZADD.ts b/packages/client/lib/commands/ZADD.ts index 5ae71a151ba..d53835d44d1 100644 --- a/packages/client/lib/commands/ZADD.ts +++ b/packages/client/lib/commands/ZADD.ts @@ -2,6 +2,9 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { SortedSetMember, transformDoubleArgument, transformDoubleReply } from './generic-transformers'; +/** + * Options for the ZADD command + */ export interface ZAddOptions { condition?: 'NX' | 'XX'; /** @@ -24,7 +27,20 @@ export interface ZAddOptions { CH?: boolean; } +/** + * Command for adding members to a sorted set + */ export default { + /** + * Constructs the ZADD command to add one or more members to a sorted set + * + * @param parser - The command parser + * @param key - The sorted set key + * @param members - One or more members to add with their scores + * @param options - Additional options for adding members + * @returns Number of new members added (or changed members if CH is set) + * @see https://redis.io/commands/zadd/ + */ parseCommand( parser: CommandParser, key: RedisArgument, @@ -59,6 +75,12 @@ export default { transformReply: transformDoubleReply } as const satisfies Command; +/** + * Helper function to push sorted set members to the command + * + * @param parser - The command parser + * @param members - One or more members with their scores + */ export function pushMembers( parser: CommandParser, members: SortedSetMember | Array) { @@ -71,6 +93,12 @@ export function pushMembers( } } +/** + * Helper function to push a single sorted set member to the command + * + * @param parser - The command parser + * @param member - Member with its score + */ function pushMember( parser: CommandParser, member: SortedSetMember diff --git a/packages/client/lib/commands/ZADD_INCR.ts b/packages/client/lib/commands/ZADD_INCR.ts index f37554b1681..73e40efe5c8 100644 --- a/packages/client/lib/commands/ZADD_INCR.ts +++ b/packages/client/lib/commands/ZADD_INCR.ts @@ -3,13 +3,33 @@ import { RedisArgument, Command } from '../RESP/types'; import { pushMembers } from './ZADD'; import { SortedSetMember, transformNullableDoubleReply } from './generic-transformers'; +/** + * Options for the ZADD INCR command + * + * @property condition - Add condition: NX (only if not exists) or XX (only if exists) + * @property comparison - Score comparison: LT (less than) or GT (greater than) + * @property CH - Return the number of changed elements instead of added elements + */ export interface ZAddOptions { condition?: 'NX' | 'XX'; comparison?: 'LT' | 'GT'; CH?: boolean; } +/** + * Command for incrementing the score of a member in a sorted set + */ export default { + /** + * Constructs the ZADD command with INCR option to increment the score of a member + * + * @param parser - The command parser + * @param key - The sorted set key + * @param members - Member(s) whose score to increment + * @param options - Additional options for the increment operation + * @returns The new score of the member after increment (null if member does not exist with XX option) + * @see https://redis.io/commands/zadd/ + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZCARD.ts b/packages/client/lib/commands/ZCARD.ts index 57b9e7f1d47..d2e0f8df5e2 100644 --- a/packages/client/lib/commands/ZCARD.ts +++ b/packages/client/lib/commands/ZCARD.ts @@ -1,9 +1,20 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; +/** + * Command for getting the number of members in a sorted set + */ export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Constructs the ZCARD command to get the cardinality (number of members) of a sorted set + * + * @param parser - The command parser + * @param key - The sorted set key + * @returns Number of members in the sorted set + * @see https://redis.io/commands/zcard/ + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZCARD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZCOUNT.ts b/packages/client/lib/commands/ZCOUNT.ts index ccbc3d13d9b..0ac473eb710 100644 --- a/packages/client/lib/commands/ZCOUNT.ts +++ b/packages/client/lib/commands/ZCOUNT.ts @@ -5,6 +5,13 @@ import { transformStringDoubleArgument } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the number of elements in the sorted set with a score between min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum score to count from (inclusive). + * @param max - Maximum score to count to (inclusive). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZDIFF.ts b/packages/client/lib/commands/ZDIFF.ts index 28135dc9c13..f52492c2bca 100644 --- a/packages/client/lib/commands/ZDIFF.ts +++ b/packages/client/lib/commands/ZDIFF.ts @@ -4,6 +4,11 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Returns the difference between the first sorted set and all the successive sorted sets. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets. + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('ZDIFF'); parser.pushKeysLength(keys); diff --git a/packages/client/lib/commands/ZDIFFSTORE.ts b/packages/client/lib/commands/ZDIFFSTORE.ts index d83a4bdc851..87407421fb0 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: true, + /** + * Computes the difference between the first and all successive sorted sets and stores it in a new key. + * @param parser - The Redis command parser. + * @param destination - Destination key where the result will be stored. + * @param inputKeys - Keys of the sorted sets to find the difference between. + */ parseCommand(parser: CommandParser, destination: RedisArgument, inputKeys: RedisVariadicArgument) { parser.push('ZDIFFSTORE'); parser.pushKey(destination); diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts index 4088f106dc6..6cb661b652a 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts @@ -6,6 +6,11 @@ import ZDIFF from './ZDIFF'; export default { IS_READ_ONLY: ZDIFF.IS_READ_ONLY, + /** + * Returns the difference between the first sorted set and all successive sorted sets with their scores. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets. + */ parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { ZDIFF.parseCommand(parser, keys); parser.push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZINCRBY.ts b/packages/client/lib/commands/ZINCRBY.ts index 5e461891e9c..30692fffb59 100644 --- a/packages/client/lib/commands/ZINCRBY.ts +++ b/packages/client/lib/commands/ZINCRBY.ts @@ -3,6 +3,13 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformDoubleArgument, transformDoubleReply } from './generic-transformers'; export default { + /** + * Increments the score of a member in a sorted set by the specified increment. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param increment - Value to increment the score by. + * @param member - Member whose score should be incremented. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZINTER.ts b/packages/client/lib/commands/ZINTER.ts index 740d3c295ec..30d6716293e 100644 --- a/packages/client/lib/commands/ZINTER.ts +++ b/packages/client/lib/commands/ZINTER.ts @@ -29,6 +29,12 @@ export function parseZInterArguments( export default { IS_READ_ONLY: true, + /** + * Intersects multiple sorted sets and returns the result as a new sorted set. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets to intersect. + * @param options - Optional parameters for the intersection operation. + */ parseCommand(parser: CommandParser, keys: ZInterKeysType, options?: ZInterOptions) { parser.push('ZINTER'); parseZInterArguments(parser, keys, options); diff --git a/packages/client/lib/commands/ZINTERCARD.ts b/packages/client/lib/commands/ZINTERCARD.ts index 8c2e98d12cb..7673b0f0a69 100644 --- a/packages/client/lib/commands/ZINTERCARD.ts +++ b/packages/client/lib/commands/ZINTERCARD.ts @@ -8,6 +8,12 @@ export interface ZInterCardOptions { export default { IS_READ_ONLY: true, + /** + * Returns the cardinality of the intersection of multiple sorted sets. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets to intersect. + * @param options - Limit option or options object with limit. + */ parseCommand( parser: CommandParser, keys: RedisVariadicArgument, diff --git a/packages/client/lib/commands/ZINTERSTORE.ts b/packages/client/lib/commands/ZINTERSTORE.ts index dcbe153cfc7..1405b70287b 100644 --- a/packages/client/lib/commands/ZINTERSTORE.ts +++ b/packages/client/lib/commands/ZINTERSTORE.ts @@ -6,6 +6,13 @@ import { parseZInterArguments, ZInterOptions } from './ZINTER'; export default { IS_READ_ONLY: false, + /** + * Stores the result of intersection of multiple sorted sets in a new sorted set. + * @param parser - The Redis command parser. + * @param destination - Destination key where the result will be stored. + * @param keys - Keys of the sorted sets to intersect. + * @param options - Optional parameters for the intersection operation. + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.ts index d3a6614b3c2..40ba3ce4287 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts @@ -5,6 +5,10 @@ import ZINTER from './ZINTER'; export default { IS_READ_ONLY: ZINTER.IS_READ_ONLY, + /** + * Intersects multiple sorted sets and returns the result with scores. + * @param args - Same parameters as ZINTER command. + */ parseCommand(...args: Parameters) { ZINTER.parseCommand(...args); args[0].push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZLEXCOUNT.ts b/packages/client/lib/commands/ZLEXCOUNT.ts index 7536590c168..97bed9f6014 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.ts @@ -4,6 +4,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the number of elements in the sorted set between the lexicographical range specified by min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum lexicographical value (inclusive). + * @param max - Maximum lexicographical value (inclusive). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZMPOP.ts b/packages/client/lib/commands/ZMPOP.ts index 0e47108e25f..fe766ddd13a 100644 --- a/packages/client/lib/commands/ZMPOP.ts +++ b/packages/client/lib/commands/ZMPOP.ts @@ -33,6 +33,13 @@ export type ZMPopArguments = Tail>; export default { IS_READ_ONLY: false, + /** + * Removes and returns up to count members with the highest/lowest scores from the first non-empty sorted set. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets to pop from. + * @param side - Side to pop from (MIN or MAX). + * @param options - Optional parameters including COUNT. + */ parseCommand( parser: CommandParser, keys: RedisVariadicArgument, diff --git a/packages/client/lib/commands/ZMSCORE.ts b/packages/client/lib/commands/ZMSCORE.ts index b225b35dfd3..0275e8d98db 100644 --- a/packages/client/lib/commands/ZMSCORE.ts +++ b/packages/client/lib/commands/ZMSCORE.ts @@ -5,6 +5,12 @@ import { createTransformNullableDoubleReplyResp2Func, RedisVariadicArgument } fr export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the scores associated with the specified members in the sorted set stored at key. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param member - One or more members to get scores for. + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('ZMSCORE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMAX.ts b/packages/client/lib/commands/ZPOPMAX.ts index 05c7f35e052..fd7b7cf9f94 100644 --- a/packages/client/lib/commands/ZPOPMAX.ts +++ b/packages/client/lib/commands/ZPOPMAX.ts @@ -4,6 +4,11 @@ import { transformDoubleReply } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes and returns the member with the highest score in the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZPOPMAX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.ts index 888ce039fbe..50f347acf3e 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts @@ -4,6 +4,12 @@ import { transformSortedSetReply } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes and returns up to count members with the highest scores in the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param count - Number of members to pop. + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('ZPOPMAX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMIN.ts b/packages/client/lib/commands/ZPOPMIN.ts index 6295925aef1..2de4977da7f 100644 --- a/packages/client/lib/commands/ZPOPMIN.ts +++ b/packages/client/lib/commands/ZPOPMIN.ts @@ -4,6 +4,11 @@ import ZPOPMAX from './ZPOPMAX'; export default { IS_READ_ONLY: false, + /** + * Removes and returns the member with the lowest score in the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZPOPMIN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.ts index 2b6abf580b9..24e084b2aef 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts @@ -4,6 +4,12 @@ import { transformSortedSetReply } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes and returns up to count members with the lowest scores in the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param count - Number of members to pop. + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('ZPOPMIN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANDMEMBER.ts b/packages/client/lib/commands/ZRANDMEMBER.ts index 2abd9d3684c..ed0a529da5e 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.ts @@ -3,6 +3,11 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type export default { IS_READ_ONLY: true, + /** + * Returns a random member from a sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZRANDMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts index 42ef8110639..f201f9c236a 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts @@ -4,6 +4,12 @@ import ZRANDMEMBER from './ZRANDMEMBER'; export default { IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY, + /** + * Returns one or more random members from a sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param count - Number of members to return. + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { ZRANDMEMBER.parseCommand(parser, key); parser.push(count.toString()); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts index f096e9d807d..3792bce794f 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -5,6 +5,12 @@ import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; export default { IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY, + /** + * Returns one or more random members with their scores from a sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param count - Number of members to return. + */ parseCommand(parser: CommandParser, key: RedisArgument, count: number) { ZRANDMEMBER_COUNT.parseCommand(parser, key, count); parser.push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZRANGE.ts b/packages/client/lib/commands/ZRANGE.ts index d1bc3433a50..43801289bde 100644 --- a/packages/client/lib/commands/ZRANGE.ts +++ b/packages/client/lib/commands/ZRANGE.ts @@ -49,6 +49,14 @@ export function zRangeArgument( export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the specified range of elements in the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum index, score or lexicographical value. + * @param max - Maximum index, score or lexicographical value. + * @param options - Optional parameters for range retrieval (BY, REV, LIMIT). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYLEX.ts b/packages/client/lib/commands/ZRANGEBYLEX.ts index 316d9745c7e..e069fa55b4b 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.ts @@ -12,6 +12,14 @@ export interface ZRangeByLexOptions { export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns all the elements in the sorted set at key with a lexicographical value between min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum lexicographical value. + * @param max - Maximum lexicographical value. + * @param options - Optional parameters including LIMIT. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.ts b/packages/client/lib/commands/ZRANGEBYSCORE.ts index 4d5471fdc0b..80bc8bc2b6c 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.ts @@ -14,6 +14,14 @@ export declare function transformReply(): Array; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns all the elements in the sorted set with a score between min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum score. + * @param max - Maximum score. + * @param options - Optional parameters including LIMIT. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts index 1a759b23dce..9cea5bd7b83 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts @@ -5,6 +5,10 @@ import ZRANGEBYSCORE from './ZRANGEBYSCORE'; export default { CACHEABLE: ZRANGEBYSCORE.CACHEABLE, IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY, + /** + * Returns all the elements in the sorted set with a score between min and max, with their scores. + * @param args - Same parameters as the ZRANGEBYSCORE command. + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZRANGESTORE.ts b/packages/client/lib/commands/ZRANGESTORE.ts index f73e93a506f..bd3e260e32c 100644 --- a/packages/client/lib/commands/ZRANGESTORE.ts +++ b/packages/client/lib/commands/ZRANGESTORE.ts @@ -13,6 +13,15 @@ export interface ZRangeStoreOptions { export default { IS_READ_ONLY: false, + /** + * Stores the result of a range operation on a sorted set into a new sorted set. + * @param parser - The Redis command parser. + * @param destination - Destination key where the result will be stored. + * @param source - Key of the source sorted set. + * @param min - Minimum index, score or lexicographical value. + * @param max - Maximum index, score or lexicographical value. + * @param options - Optional parameters for the range operation (BY, REV, LIMIT). + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts index 7e6cf00cf2e..e85af4be08e 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts @@ -5,6 +5,10 @@ import ZRANGE from './ZRANGE'; export default { CACHEABLE: ZRANGE.CACHEABLE, IS_READ_ONLY: ZRANGE.IS_READ_ONLY, + /** + * Returns the specified range of elements in the sorted set with their scores. + * @param args - Same parameters as the ZRANGE command. + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZRANK.ts b/packages/client/lib/commands/ZRANK.ts index 045e9ef8c25..73329aa2a59 100644 --- a/packages/client/lib/commands/ZRANK.ts +++ b/packages/client/lib/commands/ZRANK.ts @@ -4,6 +4,12 @@ import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the rank of a member in the sorted set, with scores ordered from low to high. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param member - Member to get the rank for. + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZRANK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts index dc2e48b362d..6f6537f4087 100644 --- a/packages/client/lib/commands/ZRANK_WITHSCORE.ts +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -4,6 +4,10 @@ import ZRANK from './ZRANK'; export default { CACHEABLE: ZRANK.CACHEABLE, IS_READ_ONLY: ZRANK.IS_READ_ONLY, + /** + * Returns the rank of a member in the sorted set with its score. + * @param args - Same parameters as the ZRANK command. + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZREM.ts b/packages/client/lib/commands/ZREM.ts index c8ba0ec02a6..960b47a36fd 100644 --- a/packages/client/lib/commands/ZREM.ts +++ b/packages/client/lib/commands/ZREM.ts @@ -4,6 +4,12 @@ import { RedisVariadicArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes the specified members from the sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param member - One or more members to remove. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.ts index 5d7e1a21bb0..434dcc6aac0 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts @@ -4,6 +4,13 @@ import { transformStringDoubleArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes all elements in the sorted set with lexicographical values between min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum lexicographical value. + * @param max - Maximum lexicographical value. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.ts index 0a2eb3fadf3..90ab6b3aefe 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts @@ -3,6 +3,13 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { IS_READ_ONLY: false, + /** + * Removes all elements in the sorted set with rank between start and stop. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param start - Minimum rank (starting from 0). + * @param stop - Maximum rank. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts index 3d23d875948..e78c57ea656 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts @@ -4,6 +4,13 @@ import { transformStringDoubleArgument } from './generic-transformers'; export default { IS_READ_ONLY: false, + /** + * Removes all elements in the sorted set with scores between min and max. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param min - Minimum score. + * @param max - Maximum score. + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREVRANK.ts b/packages/client/lib/commands/ZREVRANK.ts index d48dc68adc2..f2f79e570cc 100644 --- a/packages/client/lib/commands/ZREVRANK.ts +++ b/packages/client/lib/commands/ZREVRANK.ts @@ -4,6 +4,12 @@ import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the rank of a member in the sorted set, with scores ordered from high to low. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param member - Member to get the rank for. + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZREVRANK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZSCAN.ts b/packages/client/lib/commands/ZSCAN.ts index 051235033eb..2790db5e023 100644 --- a/packages/client/lib/commands/ZSCAN.ts +++ b/packages/client/lib/commands/ZSCAN.ts @@ -10,6 +10,13 @@ export interface HScanEntry { export default { IS_READ_ONLY: true, + /** + * Incrementally iterates over a sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param cursor - Cursor position to start the scan from. + * @param options - Optional scan parameters (COUNT, MATCH, TYPE). + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZSCORE.ts b/packages/client/lib/commands/ZSCORE.ts index 23b52901078..8b44154f44d 100644 --- a/packages/client/lib/commands/ZSCORE.ts +++ b/packages/client/lib/commands/ZSCORE.ts @@ -6,6 +6,12 @@ import { transformNullableDoubleReply } from './generic-transformers'; export default { CACHEABLE: true, IS_READ_ONLY: true, + /** + * Returns the score of a member in a sorted set. + * @param parser - The Redis command parser. + * @param key - Key of the sorted set. + * @param member - Member to get the score for. + */ parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZSCORE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZUNION.ts b/packages/client/lib/commands/ZUNION.ts index a91dc68bc09..6497d0d8e8b 100644 --- a/packages/client/lib/commands/ZUNION.ts +++ b/packages/client/lib/commands/ZUNION.ts @@ -8,6 +8,12 @@ export interface ZUnionOptions { export default { IS_READ_ONLY: true, + /** + * Returns the union of multiple sorted sets. + * @param parser - The Redis command parser. + * @param keys - Keys of the sorted sets to combine. + * @param options - Optional parameters for the union operation. + */ parseCommand(parser: CommandParser, keys: ZKeys, options?: ZUnionOptions) { parser.push('ZUNION'); parseZKeysArguments(parser, keys); diff --git a/packages/client/lib/commands/ZUNIONSTORE.ts b/packages/client/lib/commands/ZUNIONSTORE.ts index c88f5a5a6f9..9de766e8b06 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.ts @@ -8,6 +8,13 @@ export interface ZUnionOptions { export default { IS_READ_ONLY: false, + /** + * Stores the union of multiple sorted sets in a new sorted set. + * @param parser - The Redis command parser. + * @param destination - Destination key where the result will be stored. + * @param keys - Keys of the sorted sets to combine. + * @param options - Optional parameters for the union operation. + */ parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.ts index c62df55518f..af93a4eb1c0 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts @@ -5,6 +5,10 @@ import ZUNION from './ZUNION'; export default { IS_READ_ONLY: ZUNION.IS_READ_ONLY, + /** + * Returns the union of multiple sorted sets with their scores. + * @param args - Same parameters as the ZUNION command. + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts index 84997ac7d8f..842b86a0596 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts @@ -3,6 +3,11 @@ import { CommandParser } from '../../client/parser'; import { transformTuplesReply } from '../../commands/generic-transformers'; export default { + /** + * Returns information about the specified master. + * @param parser - The Redis command parser. + * @param dbname - Name of the master. + */ parseCommand(parser: CommandParser, dbname: RedisArgument) { parser.push('SENTINEL', 'MASTER', dbname); }, diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts index 65f438de132..eed4f7e7233 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts @@ -2,6 +2,14 @@ import { CommandParser } from '../../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; export default { + /** + * Instructs a Sentinel to monitor a new master with the specified parameters. + * @param parser - The Redis command parser. + * @param dbname - Name that identifies the master. + * @param host - Host of the master. + * @param port - Port of the master. + * @param quorum - Number of Sentinels that need to agree to trigger a failover. + */ parseCommand(parser: CommandParser, dbname: RedisArgument, host: RedisArgument, port: RedisArgument, quorum: RedisArgument) { parser.push('SENTINEL', 'MONITOR', dbname, host, port, quorum); }, diff --git a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts index 127449264d8..4228a2123d9 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts @@ -3,6 +3,11 @@ import { RedisArgument, ArrayReply, BlobStringReply, MapReply, Command, TypeMapp import { transformTuplesReply } from '../../commands/generic-transformers'; export default { + /** + * Returns a list of replicas for the specified master. + * @param parser - The Redis command parser. + * @param dbname - Name of the master. + */ parseCommand(parser: CommandParser, dbname: RedisArgument) { parser.push('SENTINEL', 'REPLICAS', dbname); }, diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts index 4550b9498b3..20cccbb76b6 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts @@ -3,6 +3,11 @@ import { RedisArgument, ArrayReply, MapReply, BlobStringReply, Command, TypeMapp import { transformTuplesReply } from '../../commands/generic-transformers'; export default { + /** + * Returns a list of Sentinel instances for the specified master. + * @param parser - The Redis command parser. + * @param dbname - Name of the master. + */ parseCommand(parser: CommandParser, dbname: RedisArgument) { parser.push('SENTINEL', 'SENTINELS', dbname); }, diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts index b4e8f843ea6..b2881c14e5a 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts @@ -7,6 +7,12 @@ export type SentinelSetOptions = Array<{ }>; export default { + /** + * Sets configuration parameters for a specific master. + * @param parser - The Redis command parser. + * @param dbname - Name of the master. + * @param options - Configuration options to set as option-value pairs. + */ parseCommand(parser: CommandParser, dbname: RedisArgument, options: SentinelSetOptions) { parser.push('SENTINEL', 'SET', dbname); diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index d2283b128e3..d1082baf48e 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -4,6 +4,16 @@ import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@red export default { IS_READ_ONLY: false, + /** + * Appends one or more values to the end of an array in a JSON document. + * Returns the new array length after append, or null if the path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key to append to + * @param path - Path to the array in the JSON document + * @param json - The first value to append + * @param jsons - Additional values to append + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 6ffcf9f5f0e..69485f55a6c 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -11,6 +11,18 @@ export interface JsonArrIndexOptions { export default { IS_READ_ONLY: true, + /** + * Returns the index of the first occurrence of a value in a JSON array. + * If the specified value is not found, it returns -1, or null if the path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the array + * @param path - Path to the array in the JSON document + * @param json - The value to search for + * @param options - Optional range parameters for the search + * @param options.range.start - Starting index for the search + * @param options.range.stop - Optional ending index for the search + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index e64e0b18559..33fe30a99e8 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -4,6 +4,17 @@ import { RedisJSON, transformRedisJsonArgument } from './helpers'; export default { IS_READ_ONLY: false, + /** + * Inserts one or more values into an array at the specified index. + * Returns the new array length after insert, or null if the path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the array + * @param path - Path to the array in the JSON document + * @param index - The position where to insert the values + * @param json - The first value to insert + * @param jsons - Additional values to insert + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRLEN.ts b/packages/json/lib/commands/ARRLEN.ts index f49166c218d..f2111986c0e 100644 --- a/packages/json/lib/commands/ARRLEN.ts +++ b/packages/json/lib/commands/ARRLEN.ts @@ -7,6 +7,15 @@ export interface JsonArrLenOptions { export default { IS_READ_ONLY: true, + /** + * Returns the length of an array in a JSON document. + * Returns null if the path does not exist or the value is not an array. + * + * @param parser - The Redis command parser + * @param key - The key containing the array + * @param options - Optional parameters + * @param options.path - Path to the array in the JSON document + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonArrLenOptions) { parser.push('JSON.ARRLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 30ed34c37be..53d9ed2dc87 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -10,6 +10,16 @@ export interface RedisArrPopOptions { export default { IS_READ_ONLY: false, + /** + * Removes and returns an element from an array in a JSON document. + * Returns null if the path does not exist or the value is not an array. + * + * @param parser - The Redis command parser + * @param key - The key containing the array + * @param options - Optional parameters + * @param options.path - Path to the array in the JSON document + * @param options.index - Optional index to pop from. Default is -1 (last element) + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: RedisArrPopOptions) { parser.push('JSON.ARRPOP'); parser.pushKey(key); diff --git a/packages/json/lib/commands/ARRTRIM.ts b/packages/json/lib/commands/ARRTRIM.ts index 573fa787507..bfcb1da14aa 100644 --- a/packages/json/lib/commands/ARRTRIM.ts +++ b/packages/json/lib/commands/ARRTRIM.ts @@ -3,6 +3,16 @@ import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@red export default { IS_READ_ONLY: false, + /** + * Trims an array in a JSON document to include only elements within the specified range. + * Returns the new array length after trimming, or null if the path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the array + * @param path - Path to the array in the JSON document + * @param start - Starting index (inclusive) + * @param stop - Ending index (inclusive) + */ parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, start: number, stop: number) { parser.push('JSON.ARRTRIM'); parser.pushKey(key); diff --git a/packages/json/lib/commands/CLEAR.ts b/packages/json/lib/commands/CLEAR.ts index b86513cc219..281c5e6abae 100644 --- a/packages/json/lib/commands/CLEAR.ts +++ b/packages/json/lib/commands/CLEAR.ts @@ -7,6 +7,15 @@ export interface JsonClearOptions { export default { IS_READ_ONLY: false, + /** + * Clears container values (arrays/objects) in a JSON document. + * Returns the number of values cleared (0 or 1), or null if the path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the container to clear + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonClearOptions) { parser.push('JSON.CLEAR'); parser.pushKey(key); diff --git a/packages/json/lib/commands/DEBUG_MEMORY.ts b/packages/json/lib/commands/DEBUG_MEMORY.ts index aa36d74c077..cf0f2c8f215 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.ts @@ -7,6 +7,15 @@ export interface JsonDebugMemoryOptions { export default { IS_READ_ONLY: false, + /** + * Reports memory usage details for a JSON document value. + * Returns size in bytes of the value, or null if the key or path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the value to examine + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDebugMemoryOptions) { parser.push('JSON.DEBUG', 'MEMORY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/DEL.ts b/packages/json/lib/commands/DEL.ts index e86366bebe6..4d768927088 100644 --- a/packages/json/lib/commands/DEL.ts +++ b/packages/json/lib/commands/DEL.ts @@ -7,6 +7,15 @@ export interface JsonDelOptions { export default { IS_READ_ONLY: false, + /** + * Deletes a value from a JSON document. + * Returns the number of paths deleted (0 or 1), or null if the key does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the value to delete + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDelOptions) { parser.push('JSON.DEL'); parser.pushKey(key); diff --git a/packages/json/lib/commands/FORGET.ts b/packages/json/lib/commands/FORGET.ts index 0a8ed3d91c4..ea924c74247 100644 --- a/packages/json/lib/commands/FORGET.ts +++ b/packages/json/lib/commands/FORGET.ts @@ -7,6 +7,15 @@ export interface JsonForgetOptions { export default { IS_READ_ONLY: false, + /** + * Alias for JSON.DEL - Deletes a value from a JSON document. + * Returns the number of paths deleted (0 or 1), or null if the key does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the value to delete + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonForgetOptions) { parser.push('JSON.FORGET'); parser.pushKey(key); diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index 6705ac534bc..e514fefae39 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -9,6 +9,15 @@ export interface JsonGetOptions { export default { IS_READ_ONLY: false, + /** + * Gets values from a JSON document. + * Returns the value at the specified path, or null if the key or path does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path(s) to the value(s) to retrieve + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 3c93913f91a..72baea1048a 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -4,6 +4,15 @@ import { RedisJSON, transformRedisJsonArgument } from './helpers'; export default { IS_READ_ONLY: false, + /** + * Merges a given JSON value into a JSON document. + * Returns OK on success, or null if the key does not exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Path to merge into + * @param value - JSON value to merge + */ parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, value: RedisJSON) { parser.push('JSON.MERGE'); parser.pushKey(key); diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index d0fc0a99089..7bb948bc667 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -4,6 +4,14 @@ import { transformRedisJsonNullReply } from './helpers'; export default { IS_READ_ONLY: true, + /** + * Gets values at a specific path from multiple JSON documents. + * Returns an array of values at the path from each key, null for missing keys/paths. + * + * @param parser - The Redis command parser + * @param keys - Array of keys containing JSON documents + * @param path - Path to retrieve from each document + */ parseCommand(parser: CommandParser, keys: Array, path: RedisArgument) { parser.push('JSON.MGET'); parser.pushKeys(keys); diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index 2dfab142493..9e5ec1799f1 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -10,6 +10,16 @@ export interface JsonMSetItem { export default { IS_READ_ONLY: false, + /** + * Sets multiple JSON values in multiple documents. + * Returns OK on success. + * + * @param parser - The Redis command parser + * @param items - Array of objects containing key, path, and value to set + * @param items[].key - The key containing the JSON document + * @param items[].path - Path in the document to set + * @param items[].value - JSON value to set at the path + */ parseCommand(parser: CommandParser, items: Array) { parser.push('JSON.MSET'); diff --git a/packages/json/lib/commands/NUMINCRBY.ts b/packages/json/lib/commands/NUMINCRBY.ts index 02c1c17dbc9..d8884385354 100644 --- a/packages/json/lib/commands/NUMINCRBY.ts +++ b/packages/json/lib/commands/NUMINCRBY.ts @@ -3,6 +3,15 @@ import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStr export default { IS_READ_ONLY: false, + /** + * Increments a numeric value stored in a JSON document by a given number. + * Returns the value after increment, or null if the key/path doesn't exist or value is not numeric. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Path to the numeric value + * @param by - Amount to increment by + */ parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { parser.push('JSON.NUMINCRBY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/NUMMULTBY.ts b/packages/json/lib/commands/NUMMULTBY.ts index c3621908a4c..22b25640d4b 100644 --- a/packages/json/lib/commands/NUMMULTBY.ts +++ b/packages/json/lib/commands/NUMMULTBY.ts @@ -4,6 +4,15 @@ import NUMINCRBY from './NUMINCRBY'; export default { IS_READ_ONLY: false, + /** + * Multiplies a numeric value stored in a JSON document by a given number. + * Returns the value after multiplication, or null if the key/path doesn't exist or value is not numeric. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Path to the numeric value + * @param by - Amount to multiply by + */ parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { parser.push('JSON.NUMMULTBY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/OBJKEYS.ts b/packages/json/lib/commands/OBJKEYS.ts index f7e94dd4dfc..9f8abdc42c4 100644 --- a/packages/json/lib/commands/OBJKEYS.ts +++ b/packages/json/lib/commands/OBJKEYS.ts @@ -7,6 +7,15 @@ export interface JsonObjKeysOptions { export default { IS_READ_ONLY: false, + /** + * Returns the keys in the object stored in a JSON document. + * Returns array of keys, array of arrays for multiple paths, or null if path doesn't exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the object to examine + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjKeysOptions) { parser.push('JSON.OBJKEYS'); parser.pushKey(key); diff --git a/packages/json/lib/commands/OBJLEN.ts b/packages/json/lib/commands/OBJLEN.ts index d1286a89b8c..0cee11770c1 100644 --- a/packages/json/lib/commands/OBJLEN.ts +++ b/packages/json/lib/commands/OBJLEN.ts @@ -7,6 +7,15 @@ export interface JsonObjLenOptions { export default { IS_READ_ONLY: true, + /** + * Returns the number of keys in the object stored in a JSON document. + * Returns length of object, array of lengths for multiple paths, or null if path doesn't exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the object to examine + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjLenOptions) { parser.push('JSON.OBJLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/RESP.ts b/packages/json/lib/commands/RESP.ts index 62084d73b0f..79cc2dac8d1 100644 --- a/packages/json/lib/commands/RESP.ts +++ b/packages/json/lib/commands/RESP.ts @@ -5,6 +5,14 @@ type RESPReply = Array; export default { IS_READ_ONLY: true, + /** + * Returns the JSON value at the specified path in RESP (Redis Serialization Protocol) format. + * Returns the value in RESP form, useful for language-independent processing. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Optional path to the value in the document + */ parseCommand(parser: CommandParser, key: RedisArgument, path?: string) { parser.push('JSON.RESP'); parser.pushKey(key); diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index 27da2ec64ee..a0df41fa89d 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -16,6 +16,19 @@ export interface JsonSetOptions { export default { IS_READ_ONLY: false, + /** + * Sets a JSON value at a specific path in a JSON document. + * Returns OK on success, or null if condition (NX/XX) is not met. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Path in the document to set + * @param json - JSON value to set at the path + * @param options - Optional parameters + * @param options.condition - Set condition: NX (only if doesn't exist) or XX (only if exists) + * @deprecated options.NX - Use options.condition instead + * @deprecated options.XX - Use options.condition instead + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index 3c0e5767549..aa8f3772fb1 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -8,6 +8,16 @@ export interface JsonStrAppendOptions { export default { IS_READ_ONLY: false, + /** + * Appends a string to a string value stored in a JSON document. + * Returns new string length after append, or null if the path doesn't exist or value is not a string. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param append - String to append + * @param options - Optional parameters + * @param options.path - Path to the string value + */ parseCommand(parser: CommandParser, key: RedisArgument, append: string, options?: JsonStrAppendOptions) { parser.push('JSON.STRAPPEND'); parser.pushKey(key); diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts index 644cdf27ef7..ca1923d7c6a 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -7,6 +7,15 @@ export interface JsonStrLenOptions { export default { IS_READ_ONLY: true, + /** + * Returns the length of a string value stored in a JSON document. + * Returns string length, array of lengths for multiple paths, or null if path doesn't exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to the string value + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonStrLenOptions) { parser.push('JSON.STRLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts index 85c769729c7..2d93d391164 100644 --- a/packages/json/lib/commands/TOGGLE.ts +++ b/packages/json/lib/commands/TOGGLE.ts @@ -3,6 +3,14 @@ import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@re export default { IS_READ_ONLY: false, + /** + * Toggles a boolean value stored in a JSON document. + * Returns 1 if value was toggled to true, 0 if toggled to false, or null if path doesn't exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param path - Path to the boolean value + */ parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument) { parser.push('JSON.TOGGLE'); parser.pushKey(key); diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts index 1146043b2c2..758335a7361 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -7,6 +7,15 @@ export interface JsonTypeOptions { export default { IS_READ_ONLY: true, + /** + * Returns the type of JSON value at a specific path in a JSON document. + * Returns the type as a string, array of types for multiple paths, or null if path doesn't exist. + * + * @param parser - The Redis command parser + * @param key - The key containing the JSON document + * @param options - Optional parameters + * @param options.path - Path to examine + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonTypeOptions) { parser.push('JSON.TYPE'); parser.pushKey(key); diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 0ac3d2e4ce0..9e8fb7810d6 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -141,6 +141,18 @@ export interface AggregateReply { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, + /** + * Performs an aggregation query on a RediSearch index. + * @param parser - The command parser + * @param index - The index name to query + * @param query - The text query to use as filter, use * to indicate no filtering + * @param options - Optional parameters for aggregation: + * - VERBATIM: disable stemming in query evaluation + * - LOAD: specify fields to load from documents + * - STEPS: sequence of aggregation steps (GROUPBY, SORTBY, APPLY, LIMIT, FILTER) + * - PARAMS: bind parameters for query evaluation + * - TIMEOUT: maximum time to run the query + */ parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { parser.push('FT.AGGREGATE', index, query); diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts index 8dfca7169ef..e1b0e42f9fe 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -19,6 +19,16 @@ export interface AggregateWithCursorReply extends AggregateReply { export default { IS_READ_ONLY: AGGREGATE.IS_READ_ONLY, + /** + * Performs an aggregation with a cursor for retrieving large result sets. + * @param parser - The command parser + * @param index - Name of the index to query + * @param query - The aggregation query + * @param options - Optional parameters: + * - All options supported by FT.AGGREGATE + * - COUNT: Number of results to return per cursor fetch + * - MAXIDLE: Maximum idle time for cursor in milliseconds + */ parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { AGGREGATE.parseCommand(parser, index, query, options); parser.push('WITHCURSOR'); diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts index c35e60bed4f..7d3a03498e6 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -4,6 +4,12 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Adds an alias to a RediSearch index. + * @param parser - The command parser + * @param alias - The alias to add + * @param index - The index name to alias + */ parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { parser.push('FT.ALIASADD', alias, index); }, diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts index 9a2dbda4b9e..3058be13997 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -4,6 +4,11 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Removes an existing alias from a RediSearch index. + * @param parser - The command parser + * @param alias - The alias to remove + */ parseCommand(parser: CommandParser, alias: RedisArgument) { parser.push('FT.ALIASDEL', alias); }, diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts index 3bd5ea92ba3..35879ea79cb 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/li export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Updates the index pointed to by an existing alias. + * @param parser - The command parser + * @param alias - The existing alias to update + * @param index - The new index name that the alias should point to + */ parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { parser.push('FT.ALIASUPDATE', alias, index); }, diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts index 4a68817bd2c..05c1b799eb1 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -5,6 +5,12 @@ import { RediSearchSchema, parseSchema } from './CREATE'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Alters an existing RediSearch index schema by adding new fields. + * @param parser - The command parser + * @param index - The index to alter + * @param schema - The schema definition containing new fields to add + */ parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema) { parser.push('FT.ALTER', index, 'SCHEMA', 'ADD'); parseSchema(parser, schema); diff --git a/packages/search/lib/commands/CONFIG_GET.ts b/packages/search/lib/commands/CONFIG_GET.ts index ae7a9e0c78d..8073805c533 100644 --- a/packages/search/lib/commands/CONFIG_GET.ts +++ b/packages/search/lib/commands/CONFIG_GET.ts @@ -4,6 +4,11 @@ import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Comma export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Gets a RediSearch configuration option value. + * @param parser - The command parser + * @param option - The name of the configuration option to retrieve + */ parseCommand(parser: CommandParser, option: string) { parser.push('FT.CONFIG', 'GET', option); }, diff --git a/packages/search/lib/commands/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts index 499b9525aa3..c3c8cc7259b 100644 --- a/packages/search/lib/commands/CONFIG_SET.ts +++ b/packages/search/lib/commands/CONFIG_SET.ts @@ -8,6 +8,12 @@ type FtConfigProperties = 'a' | 'b' | (string & {}) | Buffer; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Sets a RediSearch configuration option value. + * @param parser - The command parser + * @param property - The name of the configuration option to set + * @param value - The value to set for the configuration option + */ parseCommand(parser: CommandParser, property: FtConfigProperties, value: RedisArgument) { parser.push('FT.CONFIG', 'SET', property, value); }, diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index 5645a2b2dce..9f24a256fae 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -292,6 +292,22 @@ export interface CreateOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Creates a new search index with the given schema and options. + * @param parser - The command parser + * @param index - Name of the index to create + * @param schema - Index schema defining field names and types (TEXT, NUMERIC, GEO, TAG, VECTOR, GEOSHAPE) + * @param options - Optional parameters: + * - ON: Type of container to index (HASH or JSON) + * - PREFIX: Prefixes for document keys to index + * - FILTER: Expression that filters indexed documents + * - LANGUAGE/LANGUAGE_FIELD: Default language for indexing + * - SCORE/SCORE_FIELD: Document ranking parameters + * - MAXTEXTFIELDS: Index all text fields without specifying them + * - TEMPORARY: Create a temporary index + * - NOOFFSETS/NOHL/NOFIELDS/NOFREQS: Index optimization flags + * - STOPWORDS: Custom stopword list + */ parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) { parser.push('FT.CREATE', index); diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts index 5f638ebb0ee..39d0dc8af01 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -4,6 +4,12 @@ import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } f export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Deletes a cursor from an index. + * @param parser - The command parser + * @param index - The index name that contains the cursor + * @param cursorId - The cursor ID to delete + */ parseCommand(parser: CommandParser, index: RedisArgument, cursorId: UnwrapReply) { parser.push('FT.CURSOR', 'DEL', index, cursorId.toString()); }, diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts index e64070122d1..50ee5eafbd6 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -9,6 +9,14 @@ export interface FtCursorReadOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Reads from an existing cursor to get more results from an index. + * @param parser - The command parser + * @param index - The index name that contains the cursor + * @param cursor - The cursor ID to read from + * @param options - Optional parameters: + * - COUNT: Maximum number of results to return + */ parseCommand(parser: CommandParser, index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { parser.push('FT.CURSOR', 'READ', index, cursor.toString()); diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts index 2106775f854..84936ff5f73 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -5,6 +5,12 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Adds terms to a dictionary. + * @param parser - The command parser + * @param dictionary - Name of the dictionary to add terms to + * @param term - One or more terms to add to the dictionary + */ parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { parser.push('FT.DICTADD', dictionary); parser.pushVariadic(term); diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts index 988af1139e9..c39b03f45ef 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -5,6 +5,12 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Deletes terms from a dictionary. + * @param parser - The command parser + * @param dictionary - Name of the dictionary to remove terms from + * @param term - One or more terms to delete from the dictionary + */ parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { parser.push('FT.DICTDEL', dictionary); parser.pushVariadic(term); diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index 3c223442ecb..1ae40b4edb3 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -4,6 +4,11 @@ import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns all terms in a dictionary. + * @param parser - The command parser + * @param dictionary - Name of the dictionary to dump + */ parseCommand(parser: CommandParser, dictionary: RedisArgument) { parser.push('FT.DICTDUMP', dictionary); }, diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts index 407bdd031aa..5b6e0dde786 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -8,6 +8,13 @@ export interface FtDropIndexOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Deletes an index and all associated documents. + * @param parser - The command parser + * @param index - Name of the index to delete + * @param options - Optional parameters: + * - DD: Also delete the indexed documents themselves + */ parseCommand(parser: CommandParser, index: RedisArgument, options?: FtDropIndexOptions) { parser.push('FT.DROPINDEX', index); diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index 39a430f4371..78d09ffeded 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -11,6 +11,15 @@ export interface FtExplainOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the execution plan for a complex query. + * @param parser - The command parser + * @param index - Name of the index to explain query against + * @param query - The query string to explain + * @param options - Optional parameters: + * - PARAMS: Named parameters to use in the query + * - DIALECT: Version of query dialect to use (defaults to 1) + */ parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index 4ef5fba88d6..42e489ce10e 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -9,6 +9,14 @@ export interface FtExplainCLIOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the execution plan for a complex query in a more verbose format than FT.EXPLAIN. + * @param parser - The command parser + * @param index - Name of the index to explain query against + * @param query - The query string to explain + * @param options - Optional parameters: + * - DIALECT: Version of query dialect to use (defaults to 1) + */ parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index cee6ae683cd..03cf21edfd8 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -7,6 +7,11 @@ import { TuplesReply } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns information and statistics about an index. + * @param parser - The command parser + * @param index - Name of the index to get information about + */ parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.INFO', index); }, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index 94bb6984afa..99aca95a698 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -6,6 +6,15 @@ import { ProfileOptions, ProfileRawReplyResp2, ProfileReplyResp2, } from './PROF export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Profiles the execution of an aggregation query for performance analysis. + * @param parser - The command parser + * @param index - Name of the index to profile query against + * @param query - The aggregation query to profile + * @param options - Optional parameters: + * - LIMITED: Collect limited timing information only + * - All options supported by FT.AGGREGATE command + */ parseCommand( parser: CommandParser, index: string, diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index b13dbebe996..cdbb12fcdd8 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -22,6 +22,15 @@ export interface ProfileOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Profiles the execution of a search query for performance analysis. + * @param parser - The command parser + * @param index - Name of the index to profile query against + * @param query - The search query to profile + * @param options - Optional parameters: + * - LIMITED: Collect limited timing information only + * - All options supported by FT.SEARCH command + */ parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index f48ac056784..abc561dff4e 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -161,6 +161,21 @@ export function parseSearchOptions(parser: CommandParser, options?: FtSearchOpti export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Searches a RediSearch index with the given query. + * @param parser - The command parser + * @param index - The index name to search + * @param query - The text query to search. For syntax, see https://redis.io/docs/stack/search/reference/query_syntax + * @param options - Optional search parameters including: + * - VERBATIM: do not try to use stemming for query expansion + * - NOSTOPWORDS: do not filter stopwords from the query + * - INKEYS/INFIELDS: restrict the search to specific keys/fields + * - RETURN: limit which fields are returned + * - SUMMARIZE/HIGHLIGHT: create search result highlights + * - LIMIT: pagination control + * - SORTBY: sort results by a specific field + * - PARAMS: bind parameters to the query + */ parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { parser.push('FT.SEARCH', index, query); diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index a6968851acd..2fcfd2b4166 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -4,6 +4,14 @@ import SEARCH, { SearchRawReply } from './SEARCH'; export default { NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND, IS_READ_ONLY: SEARCH.IS_READ_ONLY, + /** + * Performs a search query but returns only document ids without their contents. + * @param args - Same parameters as FT.SEARCH: + * - parser: The command parser + * - index: Name of the index to search + * - query: The text query to search + * - options: Optional search parameters + */ parseCommand(...args: Parameters) { SEARCH.parseCommand(...args); args[0].push('NOCONTENT'); diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index 3b909cdca32..d6d84b19543 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -16,6 +16,16 @@ export interface FtSpellCheckOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Performs spelling correction on a search query. + * @param parser - The command parser + * @param index - Name of the index to use for spelling corrections + * @param query - The search query to check for spelling + * @param options - Optional parameters: + * - DISTANCE: Maximum Levenshtein distance for spelling suggestions + * - TERMS: Custom dictionary terms to include/exclude + * - DIALECT: Version of query dialect to use (defaults to 1) + */ parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { parser.push('FT.SPELLCHECK', index, query); diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts index 34e5bccb7f1..3fa592a2733 100644 --- a/packages/search/lib/commands/SUGADD.ts +++ b/packages/search/lib/commands/SUGADD.ts @@ -8,6 +8,16 @@ export interface FtSugAddOptions { export default { IS_READ_ONLY: true, + /** + * Adds a suggestion string to an auto-complete suggestion dictionary. + * @param parser - The command parser + * @param key - The suggestion dictionary key + * @param string - The suggestion string to add + * @param score - The suggestion score used for sorting + * @param options - Optional parameters: + * - INCR: If true, increment the existing entry's score + * - PAYLOAD: Optional payload to associate with the suggestion + */ parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) { parser.push('FT.SUGADD'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts index 6bc99456d2e..852b33f5c52 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -3,6 +3,12 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP export default { IS_READ_ONLY: true, + /** + * Deletes a string from a suggestion dictionary. + * @param parser - The command parser + * @param key - The suggestion dictionary key + * @param string - The suggestion string to delete + */ parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument) { parser.push('FT.SUGDEL'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts index e8a3aecdab0..6c463a020e2 100644 --- a/packages/search/lib/commands/SUGGET.ts +++ b/packages/search/lib/commands/SUGGET.ts @@ -8,6 +8,15 @@ export interface FtSugGetOptions { export default { IS_READ_ONLY: true, + /** + * Gets completion suggestions for a prefix from a suggestion dictionary. + * @param parser - The command parser + * @param key - The suggestion dictionary key + * @param prefix - The prefix to get completion suggestions for + * @param options - Optional parameters: + * - FUZZY: Enable fuzzy prefix matching + * - MAX: Maximum number of results to return + */ parseCommand(parser: CommandParser, key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) { parser.push('FT.SUGGET'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts index 60bf5ee86d9..a83279be0ff 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -4,6 +4,14 @@ import SUGGET from './SUGGET'; export default { IS_READ_ONLY: SUGGET.IS_READ_ONLY, + /** + * Gets completion suggestions with their payloads from a suggestion dictionary. + * @param args - Same parameters as FT.SUGGET: + * - parser: The command parser + * - key: The suggestion dictionary key + * - prefix: The prefix to get completion suggestions for + * - options: Optional parameters for fuzzy matching and max results + */ parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push('WITHPAYLOADS'); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts index 060e59132db..5c0a3fba2a3 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -9,6 +9,14 @@ type SuggestScore = { export default { IS_READ_ONLY: SUGGET.IS_READ_ONLY, + /** + * Gets completion suggestions with their scores from a suggestion dictionary. + * @param args - Same parameters as FT.SUGGET: + * - parser: The command parser + * - key: The suggestion dictionary key + * - prefix: The prefix to get completion suggestions for + * - options: Optional parameters for fuzzy matching and max results + */ parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push('WITHSCORES'); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts index 07277420338..b7aa38df3fe 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -10,6 +10,14 @@ type SuggestScoreWithPayload = { export default { IS_READ_ONLY: SUGGET.IS_READ_ONLY, + /** + * Gets completion suggestions with their scores and payloads from a suggestion dictionary. + * @param args - Same parameters as FT.SUGGET: + * - parser: The command parser + * - key: The suggestion dictionary key + * - prefix: The prefix to get completion suggestions for + * - options: Optional parameters for fuzzy matching and max results + */ parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push( diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts index a3f0fbe45ed..ecc4f4a6fc0 100644 --- a/packages/search/lib/commands/SUGLEN.ts +++ b/packages/search/lib/commands/SUGLEN.ts @@ -3,6 +3,11 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP export default { IS_READ_ONLY: true, + /** + * Gets the size of a suggestion dictionary. + * @param parser - The command parser + * @param key - The suggestion dictionary key + */ parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('FT.SUGLEN', key); }, diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 5f454f96fe0..da3e77b4223 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -4,6 +4,11 @@ import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Comm export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Dumps the contents of a synonym group. + * @param parser - The command parser + * @param index - Name of the index that contains the synonym group + */ parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.SYNDUMP', index); }, diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts index 3af735412ae..0fed14f894a 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -9,6 +9,15 @@ export interface FtSynUpdateOptions { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Updates a synonym group with new terms. + * @param parser - The command parser + * @param index - Name of the index that contains the synonym group + * @param groupId - ID of the synonym group to update + * @param terms - One or more synonym terms to add to the group + * @param options - Optional parameters: + * - SKIPINITIALSCAN: Skip the initial scan for existing documents + */ parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index 0afddb247fd..1c307945f2b 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -4,6 +4,12 @@ import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@ export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Returns the distinct values in a TAG field. + * @param parser - The command parser + * @param index - Name of the index + * @param fieldName - Name of the TAG field to get values from + */ parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) { parser.push('FT.TAGVALS', index, fieldName); }, diff --git a/packages/search/lib/commands/_LIST.ts b/packages/search/lib/commands/_LIST.ts index c1ca8cc2ee5..1b30e044e61 100644 --- a/packages/search/lib/commands/_LIST.ts +++ b/packages/search/lib/commands/_LIST.ts @@ -4,6 +4,10 @@ import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/di export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Lists all existing indexes in the database. + * @param parser - The command parser + */ parseCommand(parser: CommandParser) { parser.push('FT._LIST'); }, diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index 0f254339ff9..78a9247c41e 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -29,6 +29,14 @@ export interface TsAddOptions { export default { IS_READ_ONLY: false, + /** + * Creates or appends a sample to a time series + * @param parser - The command parser + * @param key - The key name for the time series + * @param timestamp - The timestamp of the sample + * @param value - The value of the sample + * @param options - Optional configuration parameters + */ parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index 29f99290a52..613539b4861 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -8,6 +8,12 @@ export type TsAlterOptions = Pick) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index de9cadf88c9..1e0e01164fd 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -4,6 +4,13 @@ import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RES export default { IS_READ_ONLY: false, + /** + * Deletes samples between two timestamps from a time series + * @param parser - The command parser + * @param key - The key name of the time series + * @param fromTimestamp - Start timestamp to delete from + * @param toTimestamp - End timestamp to delete until + */ parseCommand(parser: CommandParser, key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) { parser.push('TS.DEL'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts index b4e47a0fba6..8897f666b7b 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -3,6 +3,12 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li export default { IS_READ_ONLY: false, + /** + * Deletes a compaction rule between source and destination time series + * @param parser - The command parser + * @param sourceKey - The source time series key + * @param destinationKey - The destination time series key + */ parseCommand(parser: CommandParser, sourceKey: RedisArgument, destinationKey: RedisArgument) { parser.push('TS.DELETERULE'); parser.pushKeys([sourceKey, destinationKey]); diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts index c1bb2c1c749..9462705c02c 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -9,6 +9,12 @@ export type TsGetReply = TuplesReply<[]> | TuplesReply<[NumberReply, DoubleReply export default { IS_READ_ONLY: true, + /** + * Gets the last sample of a time series + * @param parser - The command parser + * @param key - The key name of the time series + * @param options - Optional parameters for the command + */ parseCommand(parser: CommandParser, key: RedisArgument, options?: TsGetOptions) { parser.push('TS.GET'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index 2365f716a83..41f11b0d7f5 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -12,6 +12,13 @@ export interface TsIncrByOptions { IGNORE?: TsIgnoreOptions; } +/** + * Parses arguments for incrementing a time series value + * @param parser - The command parser + * @param key - The key name of the time series + * @param value - The value to increment by + * @param options - Optional parameters for the command + */ export function parseIncrByArguments( parser: CommandParser, key: RedisArgument, @@ -40,6 +47,10 @@ export function parseIncrByArguments( export default { IS_READ_ONLY: false, + /** + * Increases the value of a time series by a given amount + * @param args - Arguments passed to the {@link parseIncrByArguments} function + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index 62cc1108a80..2e908a9d32d 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -73,6 +73,11 @@ export interface InfoReply { export default { IS_READ_ONLY: true, + /** + * Gets information about a time series + * @param parser - The command parser + * @param key - The key name of the time series + */ parseCommand(parser: CommandParser, key: string) { parser.push('TS.INFO'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts index 89d66a36ef8..bbdee4924ff 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -38,6 +38,11 @@ export interface InfoDebugReply extends InfoReply { export default { IS_READ_ONLY: INFO.IS_READ_ONLY, + /** + * Gets debug information about a time series + * @param parser - The command parser + * @param key - The key name of the time series + */ parseCommand(parser: CommandParser, key: string) { INFO.parseCommand(parser, key); parser.push('DEBUG'); diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index b4c91a98384..d0b36ea373f 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -10,6 +10,11 @@ export interface TsMAddSample { export default { IS_READ_ONLY: false, + /** + * Adds multiple samples to multiple time series + * @param parser - The command parser + * @param toAdd - Array of samples to add to different time series + */ parseCommand(parser: CommandParser, toAdd: Array) { parser.push('TS.MADD'); diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index fd5e8c71b93..023c0bda2d4 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -7,12 +7,22 @@ export interface TsMGetOptions { LATEST?: boolean; } +/** + * Adds LATEST argument to command if specified + * @param parser - The command parser + * @param latest - Whether to include the LATEST argument + */ export function parseLatestArgument(parser: CommandParser, latest?: boolean) { if (latest) { parser.push('LATEST'); } } +/** + * Adds FILTER argument to command + * @param parser - The command parser + * @param filter - Filter to match time series keys + */ export function parseFilterArgument(parser: CommandParser, filter: RedisVariadicArgument) { parser.push('FILTER'); parser.pushVariadic(filter); @@ -37,6 +47,12 @@ export type MGetRawReply3 = MapReply< export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Gets the last samples matching a specific filter from multiple time series + * @param parser - The command parser + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts index d74d073c174..a13fcbeaa56 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -7,6 +7,13 @@ import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; export default { IS_READ_ONLY: true, + /** + * Gets the last samples matching a specific filter with selected labels + * @param parser - The command parser + * @param filter - Filter to match time series keys + * @param selectedLabels - Labels to include in the output + * @param options - Optional parameters for the command + */ parseCommand(parser: CommandParser, filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index 737e7236130..aa9b5687eec 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -52,6 +52,12 @@ export function createTransformMGetLabelsReply() { export default { IS_READ_ONLY: true, + /** + * Gets the last samples matching a specific filter with labels + * @param parser - The command parser + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetWithLabelsOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index 3351b755499..8b9ec66e6e3 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -22,6 +22,10 @@ export type TsMRangeRawReply3 = MapReply< ]> >; +/** + * Creates a function that parses arguments for multi-range commands + * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE) + */ export function createTransformMRangeArguments(command: RedisArgument) { return ( parser: CommandParser, @@ -45,6 +49,14 @@ export function createTransformMRangeArguments(command: RedisArgument) { export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Gets samples for time series matching a specific filter within a time range + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeRawReply2, _?: any, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts index 74279d00b6d..dc049276127 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -25,6 +25,11 @@ export interface TsMRangeGroupBy { REDUCE: TimeSeriesReducer; } +/** + * Adds GROUPBY arguments to command + * @param parser - The command parser + * @param groupBy - Group by parameters + */ export function parseGroupByArguments(parser: CommandParser, groupBy: TsMRangeGroupBy) { parser.push('GROUPBY', groupBy.label, 'REDUCE', groupBy.REDUCE); } @@ -51,6 +56,10 @@ export type TsMRangeGroupByRawReply3 = MapReply< ]> >; +/** + * Creates a function that parses arguments for multi-range commands with grouping + * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE) + */ export function createTransformMRangeGroupByArguments(command: RedisArgument) { return ( parser: CommandParser, @@ -69,6 +78,10 @@ export function createTransformMRangeGroupByArguments(command: RedisArgument) { }; } +/** + * Extracts source keys from RESP3 metadata reply + * @param raw - Raw metadata from RESP3 reply + */ export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) { const unwrappedMetadata2 = raw as unknown as UnwrapReply; if (unwrappedMetadata2 instanceof Map) { @@ -82,6 +95,15 @@ export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) export default { IS_READ_ONLY: true, + /** + * Gets samples for time series matching a filter within a time range with grouping + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeGroupByArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts index 75affc54aeb..c9b737fd290 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -25,6 +25,10 @@ export type TsMRangeSelectedLabelsRawReply3 = MapReply< ]> >; +/** + * Creates a function that parses arguments for multi-range commands with selected labels + * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE) + */ export function createTransformMRangeSelectedLabelsArguments(command: RedisArgument) { return ( parser: CommandParser, @@ -50,6 +54,15 @@ export function createTransformMRangeSelectedLabelsArguments(command: RedisArgum export default { IS_READ_ONLY: true, + /** + * Gets samples for time series matching a filter with selected labels + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param selectedLabels - Labels to include in the output + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeSelectedLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { 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 99429a9bb76..d2f94b82bb3 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -17,6 +17,10 @@ export type TsMRangeWithLabelsGroupByRawReply3 = MapReply< ]> >; +/** + * Creates a function that parses arguments for multi-range commands with selected labels and grouping + * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE) + */ export function createMRangeSelectedLabelsGroupByTransformArguments( command: RedisArgument ) { @@ -47,6 +51,16 @@ export function createMRangeSelectedLabelsGroupByTransformArguments( export default { IS_READ_ONLY: true, + /** + * Gets samples for time series matching a filter with selected labels and grouping + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param selectedLabels - Labels to include in the output + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2: MRANGE_SELECTED_LABELS.transformReply[2], diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index ef4864a0307..01a3634cf4c 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -25,6 +25,10 @@ export type TsMRangeWithLabelsRawReply3 = MapReply< ]> >; +/** + * Creates a function that parses arguments for multi-range commands with labels + * @param command - The command name to use (TS.MRANGE or TS.MREVRANGE) + */ export function createTransformMRangeWithLabelsArguments(command: RedisArgument) { return ( parser: CommandParser, @@ -50,6 +54,14 @@ export function createTransformMRangeWithLabelsArguments(command: RedisArgument) export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Gets samples for time series matching a filter with labels + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeWithLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts index 6552f6328e8..08c70000f70 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -54,6 +54,15 @@ export function createMRangeWithLabelsGroupByTransformArguments(command: RedisAr export default { IS_READ_ONLY: true, + /** + * Gets samples for time series matching a filter with labels and grouping + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index 99d3123dd27..54bd5ca9cca 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -4,6 +4,14 @@ import MRANGE, { createTransformMRangeArguments } from './MRANGE'; export default { NOT_KEYED_COMMAND: MRANGE.NOT_KEYED_COMMAND, IS_READ_ONLY: MRANGE.IS_READ_ONLY, + /** + * Gets samples for time series matching a specific filter within a time range (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeArguments('TS.MREVRANGE'), transformReply: MRANGE.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts index 4afcd113505..329d9cceb8e 100644 --- a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -3,6 +3,15 @@ import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_ export default { IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY, + /** + * Gets samples for time series matching a filter within a time range with grouping (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeGroupByArguments('TS.MREVRANGE'), transformReply: MRANGE_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts index 10e00fc7a29..15dc9d87daa 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -3,6 +3,15 @@ import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } export default { IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY, + /** + * Gets samples for time series matching a filter with selected labels (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param selectedLabels - Labels to include in the output + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ 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.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts index b000c04c183..c044a9ca064 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -3,6 +3,16 @@ import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransf export default { IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY, + /** + * Gets samples for time series matching a filter with selected labels and grouping (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param selectedLabels - Labels to include in the output + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts index 6cde143c422..0a05ab2c985 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts @@ -4,6 +4,14 @@ import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './M export default { NOT_KEYED_COMMAND: MRANGE_WITHLABELS.NOT_KEYED_COMMAND, IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY, + /** + * Gets samples for time series matching a filter with labels (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param options - Optional parameters for the command + */ parseCommand: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts index 4727112b974..e5c62898951 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -3,6 +3,15 @@ import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArgume export default { IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY, + /** + * Gets samples for time series matching a filter with labels and grouping (in reverse order) + * @param parser - The command parser + * @param fromTimestamp - Start timestamp for range + * @param toTimestamp - End timestamp for range + * @param filter - Filter to match time series keys + * @param groupBy - Group by parameters + * @param options - Optional parameters for the command + */ parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 1b53e84b7a3..158a7341c8a 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -5,6 +5,11 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, + /** + * Queries the index for time series matching a specific filter + * @param parser - The command parser + * @param filter - Filter to match time series labels + */ parseCommand(parser: CommandParser, filter: RedisVariadicArgument) { parser.push('TS.QUERYINDEX'); parser.pushVariadic(filter); diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index 44da30d81de..03d58d012ca 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -101,6 +101,10 @@ export function transformRangeArguments( export default { IS_READ_ONLY: true, + /** + * Gets samples from a time series within a time range + * @param args - Arguments passed to the {@link transformRangeArguments} function + */ parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts index 238b2ce9fe7..27389b896c3 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -3,6 +3,10 @@ import RANGE, { transformRangeArguments } from './RANGE'; export default { IS_READ_ONLY: RANGE.IS_READ_ONLY, + /** + * Gets samples from a time series within a time range (in reverse order) + * @param args - Arguments passed to the {@link transformRangeArguments} function + */ parseCommand(...args: Parameters) { const parser = args[0]; From b33a662e501fcea56aa27d7e90ff604b9f965183 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 4 Jun 2025 10:52:14 +0300 Subject: [PATCH 153/244] Automate release (#2977) * release-it/bumper * remove git:false * fix package ordering * adjust git add * fix git config * adjust git config for all packages * add noop release script for test-utils * no need to try to release root * better way to handle skipping * pass parameters down * better version hint * update node version * return git arguments from before * rename release workflow * rename workflow * set git.tagMatch * add link to docs * update description * update workspace order in package-lock * fix secondary releases release-it/bumper was removing the ^ before the peerDep to client npm is not happy with that. one potential fix would be to bump all packages together as a prestep and then proceed without bupming again. for now, this fix should bring us to the previous state ( what was used in the manual process ) * require clean working dir in root * remove root release-it config not needed --- .github/workflows/release.yml | 50 + package-lock.json | 4101 +++++++++---------------- package.json | 19 +- packages/bloom/.release-it.json | 19 +- packages/bloom/package.json | 3 +- packages/client/.release-it.json | 12 +- packages/client/package.json | 3 +- packages/entraid/.release-it.json | 19 +- packages/entraid/package.json | 3 +- packages/json/.release-it.json | 19 +- packages/json/package.json | 3 +- packages/redis/.release-it.json | 22 +- packages/redis/package.json | 3 + packages/search/.release-it.json | 19 +- packages/search/package.json | 3 +- packages/time-series/.release-it.json | 19 +- packages/time-series/package.json | 3 +- 17 files changed, 1641 insertions(+), 2679 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..d5732f2eda0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release ("major", "minor", "patch", or "pre*" version; or specify version like "5.3.3")' + required: true + type: string + args: + description: 'Additional arguments to pass to release-it (e.g. "--dry-run"). See docs: https://github.com/release-it/release-it/blob/main/docs/git.md#configuration-options' + required: false + type: string + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Configure Git + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Build all packages + - name: Build packages + run: npm run build + + # Release using the monorepo approach + - name: Release packages + run: npm run release -- --ci -i ${{ github.event.inputs.version }} ${{ github.event.inputs.args }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package-lock.json b/package-lock.json index 6d2460ed19f..dddb972c236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,16 +6,24 @@ "": { "name": "redis-monorepo", "workspaces": [ - "./packages/*" + "./packages/client", + "./packages/test-utils", + "./packages/bloom", + "./packages/json", + "./packages/search", + "./packages/time-series", + "./packages/entraid", + "./packages/redis" ], "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@release-it/bumper": "^7.0.5", "@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", + "release-it": "^19.0.2", "ts-node": "^10.9.2", "tsx": "^4.7.0", "typedoc": "^0.25.7", @@ -191,24 +199,6 @@ "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", @@ -705,10 +695,343 @@ } }, "node_modules/@iarna/toml": { - "version": "2.2.5", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", "dev": true, "license": "ISC" }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.7.tgz", + "integrity": "sha512-VEr2vnI4TSM2Q50fAck98mzWJGAoxbF0rb48tcSEjkJ2kn3mM6c/YsJwnyu45PlXd6aNWObMGWmQVleL2BJy6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.11.tgz", + "integrity": "sha512-HgVha2B1lurfZ8u7cBWmu60HpkpnnIT/1IrreBx5g2oxQOVYU15WQDl6oZqjuXVbzteFKSpmMkLTMf2OmbUjaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.12.tgz", + "integrity": "sha512-uoaDadeJCYSVKYCMPwJi3AjCF9w+l9aWbHYA4iskKX84cVW/A2M6bJlWBoy3k81GpFp6EX3IElV1Z5xKw0g1QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.12.tgz", + "integrity": "sha512-YNOCY79iqI/ksWohdudGtnO02N/a2j82b6akK/+hy1/C6xoU07dsKFUBfQ36nLCxE98ICS74Uyandq7nBS31Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.14.tgz", + "integrity": "sha512-aon4yACMp4Qwc/2f6xafcC6jzAJ5vXBwL5+z4bS2y4YIOGF+QOe+Jzd5hLz1hOo+bhzVS7q07dNXTeBjaFAqRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.11.tgz", + "integrity": "sha512-gzcBWLWMiBaY507HFg4B1NJ18InnHhLjj4DTLfyoz9Rv7dSPpJ9JSj7Of8ea5QE2D+ms3ESTl/4MdzrC1//B0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.14.tgz", + "integrity": "sha512-8B4jX8ArK9zvb8/tB04jGLja4XoFfjvrTLJ5YeLlFnJh3jPa9VTQt2kxJZubGKc8YHX68e1XQxv4Nu/WZUnXIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.14.tgz", + "integrity": "sha512-/N/5PeI+QWE23dTn2D4erD9Y3yYeh0bUDkO9tt2d11mAVuCswiOKzoHrV9KYGQhoD6ae+Nff1G8TPqbfUUh8Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.2.tgz", + "integrity": "sha512-+jsUm6G9X5PUD97HkcGojzwyPsz5oSB2FUbj+D+NOYFQUj0XqvhDcDfk9mhMxFG/RDIgT9Kq4x0rm5pC5zVHUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.7", + "@inquirer/confirm": "^5.1.11", + "@inquirer/editor": "^4.2.12", + "@inquirer/expand": "^4.0.14", + "@inquirer/input": "^4.1.11", + "@inquirer/number": "^3.0.14", + "@inquirer/password": "^4.0.14", + "@inquirer/rawlist": "^4.1.2", + "@inquirer/search": "^3.0.14", + "@inquirer/select": "^4.2.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.2.tgz", + "integrity": "sha512-VDuhV58w3FuKNl24GR9ygdbu3NkGfuaK7D2gyMWeY79Lr4GVbj7ySxw1isAnelSzU1ecZC/TwICa5rCy0za2OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.14.tgz", + "integrity": "sha512-+VdtRD5nVR50K5fEMq/qbtHGH08vfqm69NJtojavlMXj6fsYymQZrNqjxEISPs2PDvtsemTJVFGs0uI6Zti6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.2.tgz", + "integrity": "sha512-3X8AAPE1WPUwY3IawT19BapD0kKpAUP7SVUu5mxmRjnl/f4q0MQz8CU8ToCC6Im0SzyOTWmSauE3GBgyOv1rBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.12", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -857,19 +1180,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@ljharb/through": { - "version": "2.3.12", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5" - }, - "engines": { - "node": ">= 0.4" - } - }, "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": { @@ -882,6 +1196,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -890,6 +1206,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": { @@ -900,8 +1218,20 @@ "node": ">= 8" } }, + "node_modules/@nodeutils/defaults-deep": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodeutils/defaults-deep/-/defaults-deep-1.1.0.tgz", + "integrity": "sha512-gG44cwQovaOFdSR02jR9IhVRpnDP64VN6JdjYJTfNz4J4fWn7TQnmrf22nSjRqlwlxPcW8PL/L3KbJg3tdwvpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.15.0" + } + }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", "dev": true, "license": "MIT", "engines": { @@ -909,175 +1239,207 @@ } }, "node_modules/@octokit/core": { - "version": "5.1.0", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", + "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.4", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "7.0.2", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/request": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { - "version": "19.1.0", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.1.5", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^12.4.0" + "@octokit/types": "^13.10.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@octokit/plugin-request-log": { - "version": "4.0.0", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", "dev": true, "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.2.0", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^12.3.0" + "@octokit/types": "^13.10.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@octokit/request": { - "version": "8.1.6", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.0", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "5.0.1", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^14.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/rest": { - "version": "20.0.2", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", + "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@octokit/core": "^6.1.4", + "@octokit/plugin-paginate-rest": "^11.4.2", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/types": { - "version": "12.4.0", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^19.1.0" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.22.0" + "@octokit/openapi-types": "^25.0.0" } }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", + "node_modules/@phun-ky/typeof": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-1.2.8.tgz", + "integrity": "sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "4.2.10" - }, "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "dev": true, - "license": "ISC" - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" + "node": "^20.9.0 || >=22.0.0", + "npm": ">=10.8.2" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/phun-ky/typeof?sponsor=1" } }, "node_modules/@redis/bloom": { @@ -1108,26 +1470,40 @@ "resolved": "packages/time-series", "link": true }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", + "node_modules/@release-it/bumper": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-7.0.5.tgz", + "integrity": "sha512-HCFMqDHreLYg4jjTWL//pW1GzZZMn3p7HDbwS2y7y5m0L6p8hEaOEixC3tEzwyVV7VP1VGjqxMvxfa360q8+Tg==", "dev": true, "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "cheerio": "^1.0.0", + "detect-indent": "7.0.1", + "fast-glob": "^3.3.3", + "ini": "^5.0.0", + "js-yaml": "^4.1.0", + "lodash-es": "^4.17.21", + "semver": "^7.7.1" + }, "engines": { - "node": ">=14.16" + "node": "^20.9.0 || >=22.0.0" }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "peerDependencies": { + "release-it": ">=18.0.0 || >=19.0.0" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "1.0.0", + "node_modules/@release-it/bumper/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=10" } }, "node_modules/@sinonjs/commons": { @@ -1169,19 +1545,10 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "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, "license": "MIT" }, @@ -1270,11 +1637,6 @@ "@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", @@ -1302,6 +1664,13 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -1406,11 +1775,10 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, "engines": { "node": ">= 14" } @@ -1427,14 +1795,6 @@ "node": ">=8" } }, - "node_modules/ansi-align": { - "version": "3.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, "node_modules/ansi-colors": { "version": "4.1.1", "dev": true, @@ -1445,6 +1805,8 @@ }, "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": { @@ -1459,6 +1821,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": { @@ -1535,21 +1899,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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", @@ -1576,47 +1925,10 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.map": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "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": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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": { @@ -1633,49 +1945,23 @@ }, "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "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" - } - ], - "license": "MIT" - }, "node_modules/basic-ftp": { - "version": "5.0.4", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "license": "MIT", "engines": { @@ -1683,7 +1969,9 @@ } }, "node_modules/before-after-hook": { - "version": "2.2.3", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "dev": true, "license": "Apache-2.0" }, @@ -1695,16 +1983,6 @@ "node": ">=8" } }, - "node_modules/bl": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "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", @@ -1747,132 +2025,12 @@ "dev": true, "license": "MIT" }, - "node_modules/boxen": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/camelcase": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "8.1.0", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "license": "ISC" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -1884,11 +2042,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1930,29 +2090,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "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" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "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", @@ -1982,40 +2119,63 @@ "node": ">= 0.8" } }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", + "node_modules/c12": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.3.tgz", + "integrity": "sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.16" + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.4.7", + "exsolve": "^1.0.4", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.1.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } } }, - "node_modules/cacheable-request": { - "version": "10.2.14", + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=14.16" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "6.0.1", + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/caching-transform": { @@ -2065,14 +2225,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "5.3.1", "dev": true, @@ -2128,9 +2280,55 @@ }, "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, "license": "MIT" }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.5.3", "dev": true, @@ -2158,7 +2356,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", "dev": true, "funding": [ { @@ -2171,38 +2371,44 @@ "node": ">=8" } }, - "node_modules/clean-stack": { - "version": "2.2.0", + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "consola": "^3.2.3" } }, - "node_modules/cli-boxes": { - "version": "3.0.0", + "node_modules/clean-stack": { + "version": "2.2.0", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/cli-cursor": { - "version": "3.1.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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": { @@ -2214,6 +2420,8 @@ }, "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": { @@ -2246,14 +2454,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "license": "Apache-2.0", @@ -2295,36 +2495,21 @@ "dev": true, "license": "MIT" }, - "node_modules/config-chain": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/configstore": { - "version": "6.0.0", + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - }, + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/yeoman/configstore?sponsor=1" + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/content-disposition": { @@ -2372,31 +2557,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.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", @@ -2417,37 +2577,44 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "4.0.0", + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">= 6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/fb55" } }, "node_modules/data-uri-to-buffer": { - "version": "4.0.1", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 14" } }, "node_modules/debug": { @@ -2477,39 +2644,6 @@ "node": ">=0.10.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/default-browser": { "version": "5.2.1", "license": "MIT", @@ -2548,25 +2682,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2595,24 +2710,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-properties": { - "version": "1.2.1", + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "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": { @@ -2634,10 +2742,12 @@ "node": ">= 0.8" } }, - "node_modules/deprecation": { - "version": "2.3.1", + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "dev": true, - "license": "ISC" + "license": "MIT" }, "node_modules/destroy": { "version": "1.2.0", @@ -2650,6 +2760,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-indent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", + "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/diff": { "version": "5.0.0", "dev": true, @@ -2658,18 +2778,63 @@ "node": ">=0.3.1" } }, - "node_modules/dot-prop": { - "version": "6.0.1", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { - "is-obj": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=10" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dotenv": { @@ -2700,11 +2865,6 @@ "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", @@ -2746,79 +2906,46 @@ "node": ">= 0.8" } }, - "node_modules/env-paths": { - "version": "2.2.1", + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.2", + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/es-abstract": { - "version": "1.22.3", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "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.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "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", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "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", @@ -2837,56 +2964,8 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "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.2", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "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", + "node_modules/es6-error": { + "version": "4.1.1", "dev": true, "license": "MIT" }, @@ -2935,17 +3014,6 @@ "node": ">=6" } }, - "node_modules/escape-goat": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "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", @@ -2966,6 +3034,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": { @@ -2998,6 +3068,8 @@ }, "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": { @@ -3006,12 +3078,27 @@ }, "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/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3033,6 +3120,8 @@ }, "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": { @@ -3055,6 +3144,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": { @@ -3064,8 +3155,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/execa/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" + }, + "engines": { + "node": ">=12" + }, + "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, "license": "ISC", "engines": { @@ -3200,8 +3309,17 @@ "dev": true, "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "dev": true, + "license": "MIT" + }, "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": { @@ -3213,8 +3331,27 @@ "node": ">=4" } }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-glob": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3222,79 +3359,22 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, "node_modules/fastq": { - "version": "1.17.1", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/figures": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/is-unicode-supported": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "dev": true, @@ -3320,7 +3400,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3405,14 +3487,6 @@ "flat": "cli.js" } }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/foreground-child": { "version": "2.0.0", "dev": true, @@ -3425,25 +3499,6 @@ "node": ">=8.0.0" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3509,31 +3564,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -3551,7 +3581,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.2.0", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, "license": "MIT", "engines": { @@ -3594,6 +3626,8 @@ }, "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": { @@ -3603,21 +3637,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-tsconfig": { "version": "4.7.2", "dev": true, @@ -3630,56 +3649,20 @@ } }, "node_modules/get-uri": { - "version": "6.0.2", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.0", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" }, "engines": { "node": ">= 14" } }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "8.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/get-uri/node_modules/jsonfile": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/get-uri/node_modules/universalify": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/gh-pages": { "version": "6.1.1", "dev": true, @@ -3701,21 +3684,43 @@ "node": ">=10" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/git-up": { - "version": "7.0.0", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", "dev": true, "license": "MIT", "dependencies": { "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" + "parse-url": "^9.2.0" } }, "node_modules/git-url-parse": { - "version": "14.0.0", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", "dev": true, "license": "MIT", "dependencies": { - "git-up": "^7.0.0" + "git-up": "^8.1.0" } }, "node_modules/glob": { @@ -3748,20 +3753,6 @@ "node": ">= 6" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globals": { "version": "11.12.0", "dev": true, @@ -3770,20 +3761,6 @@ "node": ">=4" } }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/globby": { "version": "6.1.0", "dev": true, @@ -3812,56 +3789,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "13.0.0", + "node_modules/graceful-fs": { + "version": "4.2.11", "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } + "license": "ISC" }, - "node_modules/got/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", + "node_modules/has-flag": { + "version": "4.0.0", "dev": true, "license": "MIT", "engines": { @@ -3881,17 +3815,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3905,20 +3828,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasha": { "version": "5.2.2", "dev": true, @@ -3960,10 +3869,25 @@ "dev": true, "license": "MIT" }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, - "license": "BSD-2-Clause" + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } }, "node_modules/http-errors": { "version": "2.0.0", @@ -3983,7 +3907,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -3993,23 +3919,13 @@ "node": ">= 14" } }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { - "version": "7.0.2", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -4018,6 +3934,8 @@ }, "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": { @@ -4035,64 +3953,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "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" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "dev": true, @@ -4124,130 +3984,62 @@ "license": "ISC" }, "node_modules/ini": { - "version": "2.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/inquirer": { - "version": "9.2.12", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.0.tgz", + "integrity": "sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==", "dev": true, "license": "MIT", "dependencies": { - "@ljharb/through": "^2.3.11", + "@inquirer/core": "^10.1.10", + "@inquirer/prompts": "^7.5.0", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", + "mute-stream": "^2.0.0", "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "engines": { - "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": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/ora": { - "version": "5.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "rxjs": "^7.8.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inquirer/node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" + "peerDependencies": { + "@types/node": ">=18" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/internal-slot": { - "version": "1.0.6", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" + "node": ">= 12" } }, - "node_modules/ip": { - "version": "1.1.8", + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -4259,52 +4051,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "dev": true, @@ -4316,68 +4062,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "3.0.0", "license": "MIT", @@ -4418,20 +4102,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-in-ci": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "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", "license": "MIT", @@ -4448,23 +4118,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "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": { @@ -4474,74 +4131,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-npm": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "dev": true, @@ -4550,42 +4149,10 @@ "node": ">=8" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-ssh": { - "version": "1.4.0", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -4603,48 +4170,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "dev": true, @@ -4661,17 +4186,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-windows": { "version": "1.0.2", "dev": true, @@ -4693,18 +4207,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "dev": true, "license": "ISC" }, "node_modules/issue-parser": { - "version": "6.0.0", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", "dev": true, "license": "MIT", "dependencies": { @@ -4715,7 +4226,7 @@ "lodash.uniqby": "^4.7.0" }, "engines": { - "node": ">=10.13" + "node": "^18.17 || >=20.6.1" } }, "node_modules/istanbul-lib-coverage": { @@ -4860,24 +4371,14 @@ "node": ">=8" } }, - "node_modules/iterate-iterator": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/iterate-value": { - "version": "1.0.2", + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "dev": true, "license": "MIT", - "dependencies": { - "es-get-iterator": "^1.0.2", - "iterate-iterator": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -4896,6 +4397,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, "node_modules/jsesc": { "version": "2.5.2", "dev": true, @@ -4907,16 +4415,6 @@ "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -4990,47 +4488,20 @@ "license": "MIT", "dependencies": { "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/latest-version": { - "version": "7.0.0", - "dev": true, + "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": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -5047,16 +4518,29 @@ }, "node_modules/lodash": { "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "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, "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, "license": "MIT" }, @@ -5102,6 +4586,13 @@ "version": "4.0.1", "license": "MIT" }, + "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, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -5110,6 +4601,8 @@ }, "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, "license": "MIT" }, @@ -5128,17 +4621,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -5153,7 +4635,9 @@ "license": "MIT" }, "node_modules/macos-release": { - "version": "3.2.0", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.3.0.tgz", + "integrity": "sha512-tPJQ1HeyiU2vRruNGhZ+VleWuMQRro8iFtJxYgnS4NQe+EukKF6aGiIT+7flZhISAt2iaXBCfFGvAyif7/f8nQ==", "dev": true, "license": "MIT", "engines": { @@ -5217,11 +4701,15 @@ }, "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, "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": { @@ -5239,11 +4727,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -5284,6 +4774,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": { @@ -5293,12 +4785,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "4.0.0", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5315,14 +4809,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mocha": { "version": "10.2.0", "dev": true, @@ -5416,11 +4902,13 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "1.0.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/nanoid": { @@ -5446,6 +4934,8 @@ }, "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": { @@ -5454,6 +4944,8 @@ }, "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": { @@ -5468,6 +4960,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": { @@ -5489,40 +4983,12 @@ "path-to-regexp": "^6.2.1" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", "dev": true, - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } + "license": "MIT" }, "node_modules/node-preload": { "version": "0.2.1", @@ -5548,19 +5014,10 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "8.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-run-path": { - "version": "5.2.0", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5575,6 +5032,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": { @@ -5584,6 +5043,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nyc": { "version": "15.1.0", "dev": true, @@ -5720,47 +5192,56 @@ "node": ">=6" } }, - "node_modules/object-assign": { - "version": "4.1.1", + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", "dev": true, "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.1", + "node_modules/nypm/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/object-keys": { - "version": "1.1.1", + "node_modules/object-assign": { + "version": "4.1.1", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/object.assign": { - "version": "4.1.5", + "node_modules/object-inspect": { + "version": "1.13.1", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5793,22 +5274,25 @@ } }, "node_modules/onetime": { - "version": "6.0.0", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/open": { - "version": "10.0.3", - "dev": true, + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "license": "MIT", "dependencies": { "default-browser": "^5.2.1", @@ -5824,18 +5308,20 @@ } }, "node_modules/ora": { - "version": "8.0.1", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", - "cli-cursor": "^4.0.0", + "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.1", - "string-width": "^7.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", "strip-ansi": "^7.1.0" }, "engines": { @@ -5846,7 +5332,9 @@ } }, "node_modules/ora/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -5857,7 +5345,9 @@ } }, "node_modules/ora/node_modules/chalk": { - "version": "5.3.0", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", "engines": { @@ -5867,27 +5357,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ora/node_modules/emoji-regex": { - "version": "10.3.0", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, "node_modules/ora/node_modules/is-unicode-supported": { - "version": "2.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { @@ -5899,6 +5379,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": { @@ -5914,6 +5396,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": { @@ -5923,45 +5407,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ora/node_modules/string-width": { - "version": "7.1.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5978,6 +5427,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": { @@ -5991,15 +5442,17 @@ } }, "node_modules/os-name": { - "version": "5.1.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.0.0.tgz", + "integrity": "sha512-bv608E0UX86atYi2GMGjDe0vF/X1TJjemNS8oEW6z22YW1Rc3QykSYoGfkQbX0zZX9H0ZB6CQP/3GTf1I5hURg==", "dev": true, "license": "MIT", "dependencies": { - "macos-release": "^3.1.0", - "windows-release": "^5.0.1" + "macos-release": "^3.2.0", + "windows-release": "^6.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6007,20 +5460,14 @@ }, "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, "node_modules/p-limit": { "version": "3.1.0", "dev": true, @@ -6069,174 +5516,128 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.1", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "ip": "^1.1.8", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/package-hash": { - "version": "4.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-json": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json/node_modules/got": { - "version": "12.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "node": ">= 14" } }, - "node_modules/package-json/node_modules/lru-cache": { - "version": "6.0.0", + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "degenerator": "^5.0.0", + "netmask": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">= 14" } }, - "node_modules/package-json/node_modules/semver": { - "version": "7.5.4", + "node_modules/package-hash": { + "version": "4.0.0", "dev": true, "license": "ISC", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/package-json/node_modules/yallist": { - "version": "4.0.0", + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } }, - "node_modules/parent-module": { - "version": "1.0.1", + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=14.13.0" } }, - "node_modules/parse-json": { - "version": "5.2.0", + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" + "entities": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse-path": { - "version": "7.0.0", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { - "protocols": "^2.0.0" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse-url": { - "version": "8.1.0", + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { - "parse-path": "^7.0.0" + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/parseurl": { @@ -6273,26 +5674,24 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", + "node_modules/path-to-regexp": { + "version": "6.2.1", "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "6.2.1", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "5.0.0", + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/picocolors": { "version": "1.0.0", @@ -6396,43 +5795,33 @@ "node": ">=8" } }, - "node_modules/process-on-spawn": { - "version": "1.0.0", + "node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", "dev": true, "license": "MIT", "dependencies": { - "fromentries": "^1.2.0" - }, - "engines": { - "node": ">=8" + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" } }, - "node_modules/promise.allsettled": { - "version": "1.0.7", + "node_modules/process-on-spawn": { + "version": "1.0.0", "dev": true, "license": "MIT", "dependencies": { - "array.prototype.map": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "iterate-value": "^1.0.2" + "fromentries": "^1.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/proto-list": { - "version": "1.2.4", - "dev": true, - "license": "ISC" - }, "node_modules/protocols": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", "dev": true, "license": "MIT" }, @@ -6451,18 +5840,20 @@ } }, "node_modules/proxy-agent": { - "version": "6.3.1", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", + "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" @@ -6470,6 +5861,8 @@ }, "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": { @@ -6478,23 +5871,11 @@ }, "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, "license": "MIT" }, - "node_modules/pupa": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -6513,6 +5894,8 @@ }, "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": [ { @@ -6530,17 +5913,6 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "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", @@ -6585,44 +5957,15 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "dev": true, - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "defu": "^6.1.4", + "destr": "^2.0.3" } }, "node_modules/readdirp": { @@ -6636,63 +5979,14 @@ "node": ">=8.10.0" } }, - "node_modules/rechoir": { - "version": "0.6.2", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/redis": { "resolved": "packages/redis", "link": true }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/registry-auth-token": { - "version": "5.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/registry-url": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/release-it": { - "version": "17.0.3", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.2.tgz", + "integrity": "sha512-tGRCcKeXNOMrK9Qe+ZIgQiMlQgjV8PLxZjTq1XGlCk5u1qPgx+Pps0i8HIt667FDt0wLjFtvn5o9ItpitKnVUA==", "dev": true, "funding": [ { @@ -6706,89 +6000,68 @@ ], "license": "MIT", "dependencies": { - "@iarna/toml": "2.2.5", - "@octokit/rest": "20.0.2", + "@nodeutils/defaults-deep": "1.1.0", + "@octokit/rest": "21.1.1", + "@phun-ky/typeof": "1.2.8", "async-retry": "1.3.3", - "chalk": "5.3.0", - "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.12", - "is-ci": "3.0.1", - "issue-parser": "6.0.0", - "lodash": "4.17.21", - "mime-types": "2.1.35", + "c12": "3.0.3", + "ci-info": "^4.2.0", + "eta": "3.5.0", + "git-url-parse": "16.1.0", + "inquirer": "12.6.0", + "issue-parser": "7.0.1", + "lodash.get": "4.4.2", + "lodash.merge": "4.6.2", + "mime-types": "3.0.1", "new-github-release-url": "2.0.0", - "node-fetch": "3.3.2", - "open": "10.0.3", - "ora": "8.0.1", - "os-name": "5.1.0", - "promise.allsettled": "1.0.7", - "proxy-agent": "6.3.1", - "semver": "7.5.4", - "shelljs": "0.8.5", - "update-notifier": "7.0.0", + "open": "10.1.2", + "ora": "8.2.0", + "os-name": "6.0.0", + "proxy-agent": "6.5.0", + "semver": "7.7.1", + "tinyexec": "1.0.1", + "tinyglobby": "0.2.13", + "undici": "6.21.2", "url-join": "5.0.0", - "wildcard-match": "5.1.2", + "wildcard-match": "5.1.4", "yargs-parser": "21.1.1" }, "bin": { "release-it": "bin/release-it.js" }, "engines": { - "node": ">=18" + "node": "^20.12.0 || >=22.0.0" } }, - "node_modules/release-it/node_modules/chalk": { - "version": "5.3.0", + "node_modules/release-it/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 0.6" } }, - "node_modules/release-it/node_modules/globby": { - "version": "14.0.0", + "node_modules/release-it/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/merge-streams": "^1.0.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/release-it/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, "node_modules/release-it/node_modules/semver": { - "version": "7.5.4", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6796,13 +6069,20 @@ "node": ">=10" } }, - "node_modules/release-it/node_modules/yallist": { - "version": "4.0.0", + "node_modules/release-it/node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=18.17" + } }, "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": { @@ -6833,27 +6113,6 @@ "dev": true, "license": "ISC" }, - "node_modules/resolve": { - "version": "1.22.8", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-from": { "version": "5.0.0", "dev": true, @@ -6870,56 +6129,40 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/responselike": { - "version": "3.0.0", + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "lowercase-keys": "^3.0.0" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", + "node_modules/restore-cursor/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": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "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": { @@ -6927,7 +6170,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -6961,6 +6206,8 @@ }, "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": { @@ -6969,6 +6216,8 @@ }, "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": [ { @@ -6990,30 +6239,15 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/safe-array-concat": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "funding": [ @@ -7032,22 +6266,6 @@ ], "license": "MIT" }, - "node_modules/safe-regex-test": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "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" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -7061,50 +6279,6 @@ "semver": "bin/semver.js" } }, - "node_modules/semver-diff": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-diff/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -7204,19 +6378,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7243,22 +6404,6 @@ "node": ">=8" } }, - "node_modules/shelljs": { - "version": "0.8.5", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/shiki": { "version": "0.14.7", "dev": true, @@ -7330,19 +6475,10 @@ "node": ">=8" } }, - "node_modules/slash": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "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": { @@ -7351,36 +6487,35 @@ } }, "node_modules/socks": { - "version": "2.7.1", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dev": true, "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -7422,6 +6557,8 @@ }, "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": { @@ -7431,17 +6568,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -7452,14 +6578,6 @@ "npm": ">=6" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "dev": true, @@ -7473,48 +6591,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -7536,6 +6612,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": { @@ -7589,17 +6667,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "dev": true, @@ -7613,8 +6680,62 @@ "node": ">=8" } }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "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": { @@ -7634,6 +6755,8 @@ }, "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": { @@ -7778,67 +6901,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "dev": true, @@ -7904,62 +6966,35 @@ "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, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "random-bytes": "~1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.8" } }, - "node_modules/unique-string": { - "version": "3.0.0", + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "dev": true, "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, "node_modules/universal-user-agent": { - "version": "6.0.1", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "dev": true, "license": "ISC" }, @@ -8010,85 +7045,16 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/update-notifier": { - "version": "7.0.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boxen": "^7.1.1", - "chalk": "^5.3.0", - "configstore": "^6.0.0", - "import-lazy": "^4.0.0", - "is-in-ci": "^0.1.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.5.4", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "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", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/update-notifier/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/update-notifier/node_modules/yallist": { - "version": "4.0.0", - "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", - "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", @@ -8133,20 +7099,40 @@ "dev": true, "license": "MIT" }, - "node_modules/wcwidth": { - "version": "1.0.1", + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "defaults": "^1.0.3" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.2", + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=18" } }, "node_modules/which": { @@ -8163,205 +7149,34 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.1", "dev": true, "license": "ISC" }, - "node_modules/which-typed-array": { - "version": "1.1.14", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wildcard-match": { - "version": "5.1.2", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", "dev": true, "license": "ISC" }, "node_modules/windows-release": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/windows-release/node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "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/windows-release/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/windows-release/node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/windows-release/node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/windows-release/node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/windows-release/node_modules/onetime": { - "version": "5.1.2", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "execa": "^8.0.1" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/windows-release/node_modules/strip-final-newline": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/workerpool": { "version": "6.2.1", "dev": true, @@ -8396,17 +7211,6 @@ "typedarray-to-buffer": "^3.1.5" } }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -8502,6 +7306,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/authx": { "name": "@redis/authx", "version": "5.0.0-next.5", diff --git a/package.json b/package.json index 2ff2d4825ae..e192e69d55e 100644 --- a/package.json +++ b/package.json @@ -2,26 +2,35 @@ "name": "redis-monorepo", "private": true, "workspaces": [ - "./packages/*" + "./packages/client", + "./packages/test-utils", + "./packages/bloom", + "./packages/json", + "./packages/search", + "./packages/time-series", + "./packages/entraid", + "./packages/redis" ], "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", - "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot '" + "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot '", + "release": "npm run release --workspaces --if-present --" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@release-it/bumper": "^7.0.5", "@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", + "release-it": "^19.0.2", + "ts-node": "^10.9.2", "tsx": "^4.7.0", "typedoc": "^0.25.7", - "typescript": "^5.3.3", - "ts-node": "^10.9.2" + "typescript": "^5.3.3" } } diff --git a/packages/bloom/.release-it.json b/packages/bloom/.release-it.json index 3a27a088058..23e1cf09078 100644 --- a/packages/bloom/.release-it.json +++ b/packages/bloom/.release-it.json @@ -1,11 +1,22 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "bloom@${version}", + "tagMatch": "bloom@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": ["peerDependencies.@redis/client"], + "versionPrefix": "^" + } + } } } diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 547e5ee64ed..8eb9dbf1e81 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/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'", + "release": "release-it" }, "peerDependencies": { "@redis/client": "^5.1.1" diff --git a/packages/client/.release-it.json b/packages/client/.release-it.json index 3ae247ad371..fe1a6ad0d0d 100644 --- a/packages/client/.release-it.json +++ b/packages/client/.release-it.json @@ -1,11 +1,13 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "client@${version}", + "tagMatch": "client@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" - }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" } } diff --git a/packages/client/package.json b/packages/client/package.json index a6d44451a62..9d0950eb4ea 100644 --- a/packages/client/package.json +++ b/packages/client/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'", + "release": "release-it" }, "dependencies": { "cluster-key-slot": "1.1.2" diff --git a/packages/entraid/.release-it.json b/packages/entraid/.release-it.json index a5f3a31062e..a0609a242f7 100644 --- a/packages/entraid/.release-it.json +++ b/packages/entraid/.release-it.json @@ -1,11 +1,22 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "entraid@${version}", + "tagMatch": "entraid@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": ["peerDependencies.@redis/client"], + "versionPrefix": "^" + } + } } } diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 43f26d1e039..1dc89f0c5c1 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -14,7 +14,8 @@ "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'" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'", + "release": "release-it" }, "dependencies": { "@azure/identity": "^4.7.0", diff --git a/packages/json/.release-it.json b/packages/json/.release-it.json index 8de2f3696e3..a384e3e3d4d 100644 --- a/packages/json/.release-it.json +++ b/packages/json/.release-it.json @@ -1,11 +1,22 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "json@${version}", + "tagMatch": "json@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": ["peerDependencies.@redis/client"], + "versionPrefix": "^" + } + } } } diff --git a/packages/json/package.json b/packages/json/package.json index 3b473dfce0f..09418cabf9f 100644 --- a/packages/json/package.json +++ b/packages/json/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'", + "release": "release-it" }, "peerDependencies": { "@redis/client": "^5.1.1" diff --git a/packages/redis/.release-it.json b/packages/redis/.release-it.json index 982b4ac6cbe..af127d79ae0 100644 --- a/packages/redis/.release-it.json +++ b/packages/redis/.release-it.json @@ -1,7 +1,27 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "redis@${version}", + "tagMatch": "redis@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" + }, + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": [ + "dependencies.@redis/client", + "dependencies.@redis/bloom", + "dependencies.@redis/json", + "dependencies.@redis/search", + "dependencies.@redis/time-series" + ] + } + } } } diff --git a/packages/redis/package.json b/packages/redis/package.json index 05fb70cdd11..889898b3c21 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -9,6 +9,9 @@ "dist/", "!dist/tsconfig.tsbuildinfo" ], + "scripts": { + "release": "release-it" + }, "dependencies": { "@redis/bloom": "5.1.1", "@redis/client": "5.1.1", diff --git a/packages/search/.release-it.json b/packages/search/.release-it.json index 3996a524e3b..85d55c087de 100644 --- a/packages/search/.release-it.json +++ b/packages/search/.release-it.json @@ -1,11 +1,22 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "search@${version}", + "tagMatch": "search@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": ["peerDependencies.@redis/client"], + "versionPrefix": "^" + } + } } } diff --git a/packages/search/package.json b/packages/search/package.json index 7cb73dfc0a5..6f7af2e0a25 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -10,7 +10,8 @@ ], "scripts": { "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'" + "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'", + "release": "release-it" }, "peerDependencies": { "@redis/client": "^5.1.1" diff --git a/packages/time-series/.release-it.json b/packages/time-series/.release-it.json index 6c59e8955cf..0ffec5a0c70 100644 --- a/packages/time-series/.release-it.json +++ b/packages/time-series/.release-it.json @@ -1,11 +1,22 @@ { + "npm": { + "publish": true, + "publishArgs": ["--access", "public"] + }, "git": { "tagName": "time-series@${version}", + "tagMatch": "time-series@*", "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" + "tagAnnotation": "Release ${tagName}", + "commitArgs": "--all" }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] + "plugins": { + "@release-it/bumper": { + "out": { + "file": "package.json", + "path": ["peerDependencies.@redis/client"], + "versionPrefix": "^" + } + } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 888f0f317e2..4d5d8f01be5 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/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'", + "release": "release-it" }, "peerDependencies": { "@redis/client": "^5.1.1" From 2bb515e48917b35734b3d5c7a7b03fb872db7352 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 5 Jun 2025 11:46:57 +0300 Subject: [PATCH 154/244] fix(search): align ft.search with server (#2988) as per the ft.search docs ( https://redis.io/docs/latest/commands/ft.search ): If a relevant key expires while a query is running, an attempt to load the key's value will return a null array. However, the key is still counted in the total number of results. So, instead of crashing when seeing a null as a value, we return empty object. fixes #2772 see https://github.com/redis/node-redis/pull/2814 --- packages/search/lib/commands/SEARCH.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index abc561dff4e..b8efda05777 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -221,6 +221,10 @@ export interface SearchReply { function documentValue(tuples: any) { const message = Object.create(null); + if(!tuples) { + return message; + } + let i = 0; while (i < tuples.length) { const key = tuples[i++], From 21d8f0e95799ddb8e431e58e6e9ff3f8603fba10 Mon Sep 17 00:00:00 2001 From: Navi sureka Date: Fri, 6 Jun 2025 12:00:07 +0530 Subject: [PATCH 155/244] docs(search): update SchemaFieldTypes to SCHEMA_FIELD_TYPE for redis@5.x (#2992) --- packages/search/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/search/README.md b/packages/search/README.md index 70a91fdeb2f..37597e6580a 100644 --- a/packages/search/README.md +++ b/packages/search/README.md @@ -19,11 +19,11 @@ Before we can perform any searches, we need to tell RediSearch how to index our ```javascript 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' @@ -91,15 +91,15 @@ One way we might choose to index these documents is as follows: ```javascript await client.ft.create('idx:users', { '$.name': { - type: SchemaFieldTypes.TEXT, + type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: 'UNF' }, '$.age': { - type: SchemaFieldTypes.NUMERIC, + type: SCHEMA_FIELD_TYPE.NUMERIC, AS: 'age' }, '$.coins': { - type: SchemaFieldTypes.NUMERIC, + type: SCHEMA_FIELD_TYPE.NUMERIC, AS: 'coins' } }, { From 0ebe55cbd79686b6935dbfff1bf199ec8e28b178 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:02:47 +0300 Subject: [PATCH 156/244] Release client@5.5.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 9d0950eb4ea..b95d1087d07 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 44caf9fcbad156d39c31c45f3f6ffaf0aa65dd2f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:08:35 +0300 Subject: [PATCH 157/244] Updated the Bloom package to use client@5.5.6 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dddb972c236..bd4fc1f5094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7346,12 +7346,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } }, "packages/client": { "name": "@redis/client", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.1.tgz", + "integrity": "sha512-vojbBqUdbkD+ylCy3+ZDXLzSmgiYH9pLrv87kF+nDgsRaHKrVVxPV9B4u6EfWRx7XGvQGZqsXVkKFhsEOsG3LA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.1", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 8eb9dbf1e81..af927fef0f2 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From 2302df3abdcf53cf01447b14a47f037acc34d9ba Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:09:56 +0300 Subject: [PATCH 158/244] Release bloom@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/bloom/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd4fc1f5094..c6c2cd3dd5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.1.tgz", + "integrity": "sha512-PnMcvpL7O2DHtnSL5JtyNmraNrdHuJXi3u2isGTUuPgkbAuWQKfZdknq471ySILL+qKtLfVJqzgDFMjYmZzK6Q==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.1.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index af927fef0f2..83dd6f893f9 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 91e5d306ca356624781a82a287902531237e4a2f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:11:18 +0300 Subject: [PATCH 159/244] Updated the Entraid package to use client@5.5.6 --- package-lock.json | 2 +- packages/entraid/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6c2cd3dd5c..89e00511798 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 1dc89f0c5c1..f9447291b72 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@types/express": "^4.17.21", From 7d12438300f7466ddaf73f79e9e15afe09ac25d1 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:12:16 +0300 Subject: [PATCH 160/244] Release entraid@5.5.6 --- package-lock.json | 2 +- packages/entraid/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89e00511798..b9ce55faa13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", diff --git a/packages/entraid/package.json b/packages/entraid/package.json index f9447291b72..79641cdf9a9 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From b2519abe7b53110646c2cb4edd2ceaa96d91c9e0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:13:34 +0300 Subject: [PATCH 161/244] Updated the Json package to use client@5.5.6 --- package-lock.json | 2 +- packages/json/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9ce55faa13..03fa9822402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } }, "packages/redis": { diff --git a/packages/json/package.json b/packages/json/package.json index 09418cabf9f..971f1adcce7 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From a5dc786b768549be76c2f4a0bbaef5b080ad7d86 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:14:12 +0300 Subject: [PATCH 162/244] Release json@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/json/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03fa9822402..22e3bb8c442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.1.tgz", + "integrity": "sha512-A5M0dcgxGKq+oE6spIPBcGLDBiwoSPTs2wesVb4x30rXfG6rPtqt1Z7fCMtvTL2kHUNRKgZ78zhD+0+MENZt7g==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.1", diff --git a/packages/json/package.json b/packages/json/package.json index 971f1adcce7..0d77a3bc6b1 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 8610a6c8369dfddd8814ca9d699ae56ec96a9426 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:15:45 +0300 Subject: [PATCH 163/244] Updated the Search package to use client@5.5.6 --- package-lock.json | 2 +- packages/search/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22e3bb8c442..6aff9edf361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7496,7 +7496,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 6f7af2e0a25..998d785d410 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From c9ecb3d65cf54aa735674f5c4790597eb751dc9e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:16:17 +0300 Subject: [PATCH 164/244] Release search@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/search/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6aff9edf361..95d93f347b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.1.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.1.tgz", + "integrity": "sha512-bChudQmcqfYUxEGMeXMkljXtwse4hzqcqRwbZDwRyYe+EEeW/lXVl3w/mS2tHnAb2yqGnfDghid8iHEtVNqjww==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/search/package.json b/packages/search/package.json index 998d785d410..30edac8003e 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From b07346e74a83c3c07c59126ddee3638ef7d7a359 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:18:25 +0300 Subject: [PATCH 165/244] Updated the Timeseries package to use client@5.5.6 --- package-lock.json | 2 +- packages/time-series/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95d93f347b1..9feb22bc7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7586,7 +7586,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 4d5d8f01be5..e89bab86c8c 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From 7deaf336e437dfd5cea99b4e45dd54769ae7e68e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:18:52 +0300 Subject: [PATCH 166/244] Release time-series@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/time-series/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9feb22bc7b7..fc697256e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.1.1" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.1.1.tgz", + "integrity": "sha512-HPjZLfcZxh5mBLqRgx7KCZG6JXxGnb7yJqo9qZ/KMTWK/k3SWyH47DHJbYbRNzKOEkbK/l/5kikDTm79uJuCbg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/time-series/package.json b/packages/time-series/package.json index e89bab86c8c..f90bc8f5f9b 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From ba0ba71aad841a8b7511e29deeb403260ffa8e71 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:20:13 +0300 Subject: [PATCH 167/244] Updated the Redis package to use client@5.5.6 --- package-lock.json | 70 +++---------------------------------- packages/redis/package.json | 10 +++--- 2 files changed, 10 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc697256e38..af8d1a88b7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7439,76 +7439,16 @@ "version": "5.1.1", "license": "MIT", "dependencies": { - "@redis/bloom": "5.1.1", - "@redis/client": "5.1.1", - "@redis/json": "5.1.1", - "@redis/search": "5.1.1", - "@redis/time-series": "5.1.1" + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.1.tgz", - "integrity": "sha512-PnMcvpL7O2DHtnSL5JtyNmraNrdHuJXi3u2isGTUuPgkbAuWQKfZdknq471ySILL+qKtLfVJqzgDFMjYmZzK6Q==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.1.tgz", - "integrity": "sha512-vojbBqUdbkD+ylCy3+ZDXLzSmgiYH9pLrv87kF+nDgsRaHKrVVxPV9B4u6EfWRx7XGvQGZqsXVkKFhsEOsG3LA==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/json": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.1.tgz", - "integrity": "sha512-A5M0dcgxGKq+oE6spIPBcGLDBiwoSPTs2wesVb4x30rXfG6rPtqt1Z7fCMtvTL2kHUNRKgZ78zhD+0+MENZt7g==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.1.tgz", - "integrity": "sha512-bChudQmcqfYUxEGMeXMkljXtwse4hzqcqRwbZDwRyYe+EEeW/lXVl3w/mS2tHnAb2yqGnfDghid8iHEtVNqjww==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.1.1.tgz", - "integrity": "sha512-HPjZLfcZxh5mBLqRgx7KCZG6JXxGnb7yJqo9qZ/KMTWK/k3SWyH47DHJbYbRNzKOEkbK/l/5kikDTm79uJuCbg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/redis/package.json b/packages/redis/package.json index 889898b3c21..bdead544fde 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.1.1", - "@redis/client": "5.1.1", - "@redis/json": "5.1.1", - "@redis/search": "5.1.1", - "@redis/time-series": "5.1.1" + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" }, "engines": { "node": ">= 18" From ca91718b594aa624571639abfe5bb8195637bc53 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:21:11 +0300 Subject: [PATCH 168/244] Release redis@5.5.6 --- package-lock.json | 2 +- packages/redis/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index af8d1a88b7f..d9fc9f93f92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,7 +7436,7 @@ } }, "packages/redis": { - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "dependencies": { "@redis/bloom": "5.5.6", diff --git a/packages/redis/package.json b/packages/redis/package.json index bdead544fde..bf5ea798e70 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.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 62ac8b7c32473b9d0e45cbb628d05a910bc00a5f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 15:38:52 +0300 Subject: [PATCH 169/244] fix(client): make unstable cmds throw (#2990) As per the docs, unstableResp3 commands should throw when client is created with { RESP: 3, unstableResp3: false|undefined } fixes #2989 --- docs/v5.md | 4 +-- packages/client/lib/commander.ts | 6 +++- packages/client/lib/commands/XREAD.spec.ts | 33 +++++++++++++++++++ .../client/lib/commands/XREADGROUP.spec.ts | 32 ++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/docs/v5.md b/docs/v5.md index 1784ae5bd74..15ef67c14ee 100644 --- a/docs/v5.md +++ b/docs/v5.md @@ -42,9 +42,9 @@ RESP3 uses a different mechanism for handling Pub/Sub messages. Instead of modif ## Known Limitations -### Unstable Module Commands +### Unstable 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: +Some Redis 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({ diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index 6e5a2687cb1..cfdf39526cc 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -38,7 +38,11 @@ export function attachConfig< Class: any = class extends BaseClass {}; for (const [name, command] of Object.entries(commands)) { - Class.prototype[name] = createCommand(command, RESP); + if (config?.RESP == 3 && command.unstableResp3 && !config.unstableResp3) { + Class.prototype[name] = throwResp3SearchModuleUnstableError; + } else { + Class.prototype[name] = createCommand(command, RESP); + } } if (config?.modules) { diff --git a/packages/client/lib/commands/XREAD.spec.ts b/packages/client/lib/commands/XREAD.spec.ts index bb72c96497e..0edcfe43117 100644 --- a/packages/client/lib/commands/XREAD.spec.ts +++ b/packages/client/lib/commands/XREAD.spec.ts @@ -131,4 +131,37 @@ describe('XREAD', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testWithClient('client.xRead should throw with resp3 and unstableResp3: false', async client => { + assert.throws( + () => client.xRead({ + key: 'key', + id: '0-0' + }), + { + message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance' + } + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + } + }); + + testUtils.testWithClient('client.xRead should not throw with resp3 and unstableResp3: true', async client => { + assert.doesNotThrow( + () => client.xRead({ + key: 'key', + id: '0-0' + }) + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + unstableResp3: true + } + }); + }); diff --git a/packages/client/lib/commands/XREADGROUP.spec.ts b/packages/client/lib/commands/XREADGROUP.spec.ts index 085a67bc9b3..acc7cc2dea9 100644 --- a/packages/client/lib/commands/XREADGROUP.spec.ts +++ b/packages/client/lib/commands/XREADGROUP.spec.ts @@ -155,4 +155,36 @@ describe('XREADGROUP', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testWithClient('client.xReadGroup should throw with resp3 and unstableResp3: false', async client => { + assert.throws( + () => client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + { + message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance' + } + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + } + }); + + testUtils.testWithClient('client.xReadGroup should not throw with resp3 and unstableResp3: true', async client => { + assert.doesNotThrow( + () => client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + unstableResp3: true + } + }); }); From 5d205cf161acce0111d344ef625e3ac4a0488b76 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 16 Jun 2025 10:20:49 +0300 Subject: [PATCH 170/244] chore(tests): bump test container version 8.0.2 (#2997) --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/sentinel/test-util.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/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 395e0251018..d7bd649900d 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.1-pre"] + redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 71b423b41ea..62a1f40e872 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-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 60c1a59689a..06f8f74093c 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 19bbafc66ea..48736b56cd0 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 11ad498f0b3..1beac939138 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-M05-pre' + defaultDockerVersion: '8.0.2' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 9894b2d0399..0d250449ac0 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-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 7264b1b6b12..354ca16a287 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-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 0f25341e34d..ab3f1cebf7d 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-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { From 2b3140bb72657af328c8b0f814c4901b0dd2ed52 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 19 Jun 2025 10:05:22 +0300 Subject: [PATCH 171/244] chore(actions): add action to close stale issues (#3000) --- .github/workflows/stale-issues.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/stale-issues.yml diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 00000000000..445af1c818c --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,25 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" + +permissions: {} +jobs: + stale: + permissions: + issues: write # to close stale issues (actions/stale) + pull-requests: write # to close stale PRs (actions/stale) + + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' + stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' + days-before-stale: 365 + days-before-close: 30 + stale-issue-label: "Stale" + stale-pr-label: "Stale" + operations-per-run: 10 + remove-stale-when-updated: true From b52177752e1120f9e587b0a0062019a472f59af1 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:56:00 +0300 Subject: [PATCH 172/244] feat: added support for new bitop operations (#3001) refactored bitop tests, covered all operations updated default docker version on all packages --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/commands/BITOP.spec.ts | 70 +++++++++++++++++++++- packages/client/lib/commands/BITOP.ts | 4 +- packages/client/lib/sentinel/test-util.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/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 10 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d7bd649900d..89efdb61114 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.2"] + redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2", "8.2-M01-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 62a1f40e872..4396c94f726 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.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/commands/BITOP.spec.ts b/packages/client/lib/commands/BITOP.spec.ts index 25fe48fc13c..65fe6f86338 100644 --- a/packages/client/lib/commands/BITOP.spec.ts +++ b/packages/client/lib/commands/BITOP.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import BITOP from './BITOP'; +import BITOP, { BitOperations } from './BITOP'; import { parseArgs } from './generic-transformers'; describe('BITOP', () => { @@ -20,13 +20,77 @@ describe('BITOP', () => { }); }); - testUtils.testAll('bitOp', async client => { + for (const op of ['AND', 'OR', 'XOR'] as BitOperations[]) { + testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => { + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll(`bitOp ${op} with existing keys`, async client => { + await client.set('{tag}key1', 'value1'); + await client.set('{tag}key2', 'value2'); + + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 6 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + } + + // NOT operation requires only one key + testUtils.testAll('bitOp NOT with non-existing keys', async client => { assert.equal( - await client.bitOp('AND', '{tag}destKey', '{tag}key'), + await client.bitOp('NOT', '{tag}destKey', '{tag}key'), 0 ); }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testAll('bitOp NOT with existing keys', async client => { + await client.set('{tag}key', 'value'); + + assert.equal( + await client.bitOp('NOT', '{tag}destKey', '{tag}key'), + 5 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + // newer operations supported since Redis 8.2 + for (const op of ['DIFF', 'DIFF1', 'ANDOR', 'ONE'] as BitOperations[]) { + testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => { + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 0 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + }); + + testUtils.testAll(`bitOp ${op} with existing keys`, async client => { + await client.set('{tag}key1', 'value1'); + await client.set('{tag}key2', 'value2'); + + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 6 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + }); + } }); diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts index cfce482fcb5..da8b97ceda6 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -2,14 +2,14 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; -export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT'; +export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT' | 'DIFF' | 'DIFF1' | 'ANDOR' | 'ONE'; export default { IS_READ_ONLY: false, /** * Performs bitwise operations between strings * @param parser - The Redis command parser - * @param operation - Bitwise operation to perform: AND, OR, XOR, NOT + * @param operation - Bitwise operation to perform: AND, OR, XOR, NOT, DIFF, DIFF1, ANDOR, ONE * @param destKey - Destination key to store the result * @param key - Source key(s) to perform operation on */ diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 06f8f74093c..c8efa47f41d 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 48736b56cd0..809ee788e92 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 1beac939138..3c561d4ba44 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.2' + defaultDockerVersion: '8.2-M01-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 0d250449ac0..6b6859d61bf 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.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 354ca16a287..a2b9c816da1 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.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index ab3f1cebf7d..8a664ee8df2 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.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { From c5b4f47975efea2347d923a32326808ce15a19be Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 24 Jun 2025 13:35:29 +0300 Subject: [PATCH 173/244] feat: add support for vector sets (#2998) * wip * improve the vadd api * resp3 tests * fix some tests * extract json helper functions in client package * use transformJsonReply * remove the CACHEABLE flag for all vector set commands currently, client side caching is not supported for vector set commands by the server * properly transform vinfo result * add resp3 test for vlinks * add more tests for vrandmember * fix vrem return types * fix vsetattr return type * fix vsim_withscores * implement vlinks_withscores * set minimum docker image version to 8 * align return types * add RAW variant for VEMB -> VEMB_RAW * use the new parseCommand api --- packages/client/lib/commands/VADD.spec.ts | 121 +++++++++++ packages/client/lib/commands/VADD.ts | 65 ++++++ packages/client/lib/commands/VCARD.spec.ts | 60 ++++++ packages/client/lib/commands/VCARD.ts | 18 ++ packages/client/lib/commands/VDIM.spec.ts | 43 ++++ packages/client/lib/commands/VDIM.ts | 18 ++ packages/client/lib/commands/VEMB.spec.ts | 42 ++++ packages/client/lib/commands/VEMB.ts | 21 ++ packages/client/lib/commands/VEMB_RAW.spec.ts | 68 ++++++ packages/client/lib/commands/VEMB_RAW.ts | 57 +++++ packages/client/lib/commands/VGETATTR.spec.ts | 77 +++++++ packages/client/lib/commands/VGETATTR.ts | 21 ++ packages/client/lib/commands/VINFO.spec.ts | 58 +++++ packages/client/lib/commands/VINFO.ts | 38 ++++ packages/client/lib/commands/VLINKS.spec.ts | 42 ++++ packages/client/lib/commands/VLINKS.ts | 20 ++ .../lib/commands/VLINKS_WITHSCORES.spec.ts | 75 +++++++ .../client/lib/commands/VLINKS_WITHSCORES.ts | 42 ++++ .../client/lib/commands/VRANDMEMBER.spec.ts | 201 ++++++++++++++++++ packages/client/lib/commands/VRANDMEMBER.ts | 23 ++ packages/client/lib/commands/VREM.spec.ts | 63 ++++++ packages/client/lib/commands/VREM.ts | 20 ++ packages/client/lib/commands/VSETATTR.spec.ts | 58 +++++ packages/client/lib/commands/VSETATTR.ts | 32 +++ packages/client/lib/commands/VSIM.spec.ts | 85 ++++++++ packages/client/lib/commands/VSIM.ts | 68 ++++++ .../lib/commands/VSIM_WITHSCORES.spec.ts | 62 ++++++ .../client/lib/commands/VSIM_WITHSCORES.ts | 36 ++++ .../lib/commands/generic-transformers.ts | 18 ++ packages/client/lib/commands/index.ts | 44 +++- packages/json/lib/commands/ARRAPPEND.ts | 2 +- packages/json/lib/commands/ARRINDEX.ts | 2 +- packages/json/lib/commands/ARRINSERT.ts | 2 +- packages/json/lib/commands/ARRPOP.ts | 3 +- packages/json/lib/commands/GET.ts | 3 +- packages/json/lib/commands/MERGE.ts | 2 +- packages/json/lib/commands/MGET.ts | 2 +- packages/json/lib/commands/MSET.ts | 2 +- packages/json/lib/commands/SET.ts | 2 +- packages/json/lib/commands/STRAPPEND.ts | 2 +- packages/json/lib/commands/helpers.ts | 20 -- packages/json/lib/commands/index.ts | 4 +- 42 files changed, 1608 insertions(+), 34 deletions(-) create mode 100644 packages/client/lib/commands/VADD.spec.ts create mode 100644 packages/client/lib/commands/VADD.ts create mode 100644 packages/client/lib/commands/VCARD.spec.ts create mode 100644 packages/client/lib/commands/VCARD.ts create mode 100644 packages/client/lib/commands/VDIM.spec.ts create mode 100644 packages/client/lib/commands/VDIM.ts create mode 100644 packages/client/lib/commands/VEMB.spec.ts create mode 100644 packages/client/lib/commands/VEMB.ts create mode 100644 packages/client/lib/commands/VEMB_RAW.spec.ts create mode 100644 packages/client/lib/commands/VEMB_RAW.ts create mode 100644 packages/client/lib/commands/VGETATTR.spec.ts create mode 100644 packages/client/lib/commands/VGETATTR.ts create mode 100644 packages/client/lib/commands/VINFO.spec.ts create mode 100644 packages/client/lib/commands/VINFO.ts create mode 100644 packages/client/lib/commands/VLINKS.spec.ts create mode 100644 packages/client/lib/commands/VLINKS.ts create mode 100644 packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts create mode 100644 packages/client/lib/commands/VLINKS_WITHSCORES.ts create mode 100644 packages/client/lib/commands/VRANDMEMBER.spec.ts create mode 100644 packages/client/lib/commands/VRANDMEMBER.ts create mode 100644 packages/client/lib/commands/VREM.spec.ts create mode 100644 packages/client/lib/commands/VREM.ts create mode 100644 packages/client/lib/commands/VSETATTR.spec.ts create mode 100644 packages/client/lib/commands/VSETATTR.ts create mode 100644 packages/client/lib/commands/VSIM.spec.ts create mode 100644 packages/client/lib/commands/VSIM.ts create mode 100644 packages/client/lib/commands/VSIM_WITHSCORES.spec.ts create mode 100644 packages/client/lib/commands/VSIM_WITHSCORES.ts delete mode 100644 packages/json/lib/commands/helpers.ts diff --git a/packages/client/lib/commands/VADD.spec.ts b/packages/client/lib/commands/VADD.spec.ts new file mode 100644 index 00000000000..e064beab498 --- /dev/null +++ b/packages/client/lib/commands/VADD.spec.ts @@ -0,0 +1,121 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VADD from './VADD'; +import { BasicCommandParser } from '../client/parser'; + +describe('VADD', () => { + describe('parseCommand', () => { + it('basic usage', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0, 3.0], 'element'); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '3', '1', '2', '3', 'element'] + ); + }); + + it('with REDUCE option', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2], 'element', { REDUCE: 50 }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element'] + ); + }); + + it('with quantization options', () => { + let parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'Q8' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'Q8'] + ); + + parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'BIN' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'BIN'] + ); + + parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'NOQUANT' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'NOQUANT'] + ); + }); + + it('with all options', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { + REDUCE: 50, + CAS: true, + QUANT: 'Q8', + EF: 200, + SETATTR: { name: 'test', value: 42 }, + M: 16 + }); + assert.deepEqual( + parser.redisArgs, + [ + 'VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element', + 'CAS', 'Q8', 'EF', '200', 'SETATTR', '{"name":"test","value":42}', 'M', '16' + ] + ); + }); + }); + + testUtils.testAll('vAdd', async client => { + assert.equal( + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'), + true + ); + + // same element should not be added again + assert.equal( + await client.vAdd('key', [1, 2 , 3], 'element'), + false + ); + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }, + }); + + testUtils.testWithClient('vAdd with RESP3', async client => { + // Test basic functionality with RESP3 + assert.equal( + await client.vAdd('resp3-key', [1.5, 2.5, 3.5], 'resp3-element'), + true + ); + + // same element should not be added again + assert.equal( + await client.vAdd('resp3-key', [1, 2 , 3], 'resp3-element'), + false + ); + + // Test with options to ensure complex parameters work with RESP3 + assert.equal( + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'resp3-element2', { + QUANT: 'Q8', + CAS: true, + SETATTR: { type: 'test', value: 123 } + }), + true + ); + + // Verify the vector set was created correctly + assert.equal( + await client.vCard('resp3-key'), + 2 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VADD.ts b/packages/client/lib/commands/VADD.ts new file mode 100644 index 00000000000..0406bd58d03 --- /dev/null +++ b/packages/client/lib/commands/VADD.ts @@ -0,0 +1,65 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply, transformDoubleArgument } from './generic-transformers'; + +export interface VAddOptions { + REDUCE?: number; + CAS?: boolean; + QUANT?: 'NOQUANT' | 'BIN' | 'Q8', + EF?: number; + SETATTR?: Record; + M?: number; +} + +export default { + /** + * Add a new element into the vector set specified by key + * + * @param parser - The command parser + * @param key - The name of the key that will hold the vector set data + * @param vector - The vector data as array of numbers + * @param element - The name of the element being added to the vector set + * @param options - Optional parameters for vector addition + * @see https://redis.io/commands/vadd/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + vector: Array, + element: RedisArgument, + options?: VAddOptions + ) { + parser.push('VADD'); + parser.pushKey(key); + + if (options?.REDUCE !== undefined) { + parser.push('REDUCE', options.REDUCE.toString()); + } + + parser.push('VALUES', vector.length.toString()); + for (const value of vector) { + parser.push(transformDoubleArgument(value)); + } + + parser.push(element); + + if (options?.CAS) { + parser.push('CAS'); + } + + options?.QUANT && parser.push(options.QUANT); + + if (options?.EF !== undefined) { + parser.push('EF', options.EF.toString()); + } + + if (options?.SETATTR) { + parser.push('SETATTR', JSON.stringify(options.SETATTR)); + } + + if (options?.M !== undefined) { + parser.push('M', options.M.toString()); + } + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VCARD.spec.ts b/packages/client/lib/commands/VCARD.spec.ts new file mode 100644 index 00000000000..feb9040fcb7 --- /dev/null +++ b/packages/client/lib/commands/VCARD.spec.ts @@ -0,0 +1,60 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VCARD from './VCARD'; +import { BasicCommandParser } from '../client/parser'; + +describe('VCARD', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VCARD.parseCommand(parser, 'key') + assert.deepEqual( + parser.redisArgs, + ['VCARD', 'key'] + ); + }); + + testUtils.testAll('vCard', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + assert.equal( + await client.vCard('key'), + 2 + ); + + assert.equal(await client.vCard('unknown'), 0); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vCard with RESP3', async client => { + // Test empty vector set + assert.equal( + await client.vCard('resp3-empty-key'), + 0 + ); + + // Add elements and test cardinality + await client.vAdd('resp3-key', [1.0, 2.0], 'elem1'); + assert.equal( + await client.vCard('resp3-key'), + 1 + ); + + await client.vAdd('resp3-key', [3.0, 4.0], 'elem2'); + await client.vAdd('resp3-key', [5.0, 6.0], 'elem3'); + assert.equal( + await client.vCard('resp3-key'), + 3 + ); + + assert.equal(await client.vCard('unknown'), 0); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VCARD.ts b/packages/client/lib/commands/VCARD.ts new file mode 100644 index 00000000000..575abf9b710 --- /dev/null +++ b/packages/client/lib/commands/VCARD.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the number of elements in a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vcard/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VCARD'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VDIM.spec.ts b/packages/client/lib/commands/VDIM.spec.ts new file mode 100644 index 00000000000..db3f5f3bd8f --- /dev/null +++ b/packages/client/lib/commands/VDIM.spec.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VDIM from './VDIM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VDIM', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VDIM.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VDIM', 'key'] + ); + }); + + testUtils.testAll('vDim', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vDim('key'), + 3 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vDim with RESP3', async client => { + await client.vAdd('resp3-5d', [1.0, 2.0, 3.0, 4.0, 5.0], 'elem5d'); + + assert.equal( + await client.vDim('resp3-5d'), + 5 + ); + + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VDIM.ts b/packages/client/lib/commands/VDIM.ts new file mode 100644 index 00000000000..f7933e77eac --- /dev/null +++ b/packages/client/lib/commands/VDIM.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the dimension of the vectors in a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vdim/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VDIM'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VEMB.spec.ts b/packages/client/lib/commands/VEMB.spec.ts new file mode 100644 index 00000000000..ed9515ebddf --- /dev/null +++ b/packages/client/lib/commands/VEMB.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VEMB from './VEMB'; +import { BasicCommandParser } from '../client/parser'; + +describe('VEMB', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VEMB.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VEMB', 'key', 'element'] + ); + }); + + testUtils.testAll('vEmb', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const result = await client.vEmb('key', 'element'); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 3); + assert.equal(typeof result[0], 'number'); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vEmb with RESP3', async client => { + await client.vAdd('resp3-key', [1.5, 2.5, 3.5, 4.5], 'resp3-element'); + + const result = await client.vEmb('resp3-key', 'resp3-element'); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 4); + assert.equal(typeof result[0], 'number'); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VEMB.ts b/packages/client/lib/commands/VEMB.ts new file mode 100644 index 00000000000..d534c27d65d --- /dev/null +++ b/packages/client/lib/commands/VEMB.ts @@ -0,0 +1,21 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformDoubleArrayReply } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the approximate vector associated with a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve the vector for + * @see https://redis.io/commands/vemb/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VEMB'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformDoubleArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VEMB_RAW.spec.ts b/packages/client/lib/commands/VEMB_RAW.spec.ts new file mode 100644 index 00000000000..33d3af8540d --- /dev/null +++ b/packages/client/lib/commands/VEMB_RAW.spec.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VEMB_RAW from './VEMB_RAW'; +import { BasicCommandParser } from '../client/parser'; + +describe('VEMB_RAW', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VEMB_RAW.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VEMB', 'key', 'element', 'RAW'] + ); + }); + + testUtils.testAll('vEmbRaw', async client => { + await client.vAdd('key1', [1.0, 2.0, 3.0], 'element'); + const result1 = await client.vEmbRaw('key1', 'element'); + assert.equal(result1.quantization, 'int8'); + assert.ok(result1.quantizationRange !== undefined); + + await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' }); + const result2 = await client.vEmbRaw('key2', 'element'); + assert.equal(result2.quantization, 'int8'); + assert.ok(result2.quantizationRange !== undefined); + + await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' }); + const result3 = await client.vEmbRaw('key3', 'element'); + assert.equal(result3.quantization, 'f32'); + assert.equal(result3.quantizationRange, undefined); + + await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' }); + const result4 = await client.vEmbRaw('key4', 'element'); + assert.equal(result4.quantization, 'bin'); + assert.equal(result4.quantizationRange, undefined); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vEmbRaw with RESP3', async client => { + await client.vAdd('key1', [1.0, 2.0, 3.0], 'element'); + const result1 = await client.vEmbRaw('key1', 'element'); + assert.equal(result1.quantization, 'int8'); + assert.ok(result1.quantizationRange !== undefined); + + await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' }); + const result2 = await client.vEmbRaw('key2', 'element'); + assert.equal(result2.quantization, 'int8'); + assert.ok(result2.quantizationRange !== undefined); + + await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' }); + const result3 = await client.vEmbRaw('key3', 'element'); + assert.equal(result3.quantization, 'f32'); + assert.equal(result3.quantizationRange, undefined); + + await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' }); + const result4 = await client.vEmbRaw('key4', 'element'); + assert.equal(result4.quantization, 'bin'); + assert.equal(result4.quantizationRange, undefined); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VEMB_RAW.ts b/packages/client/lib/commands/VEMB_RAW.ts new file mode 100644 index 00000000000..b6881d321c9 --- /dev/null +++ b/packages/client/lib/commands/VEMB_RAW.ts @@ -0,0 +1,57 @@ +import { CommandParser } from '../client/parser'; +import { + RedisArgument, + Command, + BlobStringReply, + SimpleStringReply, + DoubleReply +} from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VEMB from './VEMB'; + +type RawVembReply = { + quantization: SimpleStringReply; + raw: BlobStringReply; + l2Norm: DoubleReply; + quantizationRange?: DoubleReply; +}; + +const transformRawVembReply = { + 2: (reply: any[]): RawVembReply => { + return { + quantization: reply[0], + raw: reply[1], + l2Norm: transformDoubleReply[2](reply[2]), + ...(reply[3] !== undefined && { quantizationRange: transformDoubleReply[2](reply[3]) }) + }; + }, + 3: (reply: any[]): RawVembReply => { + return { + quantization: reply[0], + raw: reply[1], + l2Norm: reply[2], + quantizationRange: reply[3] + }; + }, +}; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the RAW approximate vector associated with a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve the vector for + * @see https://redis.io/commands/vemb/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + element: RedisArgument + ) { + VEMB.parseCommand(parser, key, element); + parser.push('RAW'); + }, + transformReply: transformRawVembReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VGETATTR.spec.ts b/packages/client/lib/commands/VGETATTR.spec.ts new file mode 100644 index 00000000000..d904146c670 --- /dev/null +++ b/packages/client/lib/commands/VGETATTR.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VGETATTR from './VGETATTR'; +import { BasicCommandParser } from '../client/parser'; + +describe('VGETATTR', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VGETATTR.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VGETATTR', 'key', 'element'] + ); + }); + + testUtils.testAll('vGetAttr', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const nullResult = await client.vGetAttr('key', 'element'); + assert.equal(nullResult, null); + + await client.vSetAttr('key', 'element', { name: 'test' }); + + const result = await client.vGetAttr('key', 'element'); + + assert.ok(result !== null); + assert.equal(typeof result, 'object') + + assert.deepEqual(result, { + name: 'test' + }) + + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vGetAttr with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0], 'resp3-element'); + + // Test null case (no attributes set) + const nullResult = await client.vGetAttr('resp3-key', 'resp3-element'); + + assert.equal(nullResult, null); + + // Set complex attributes and retrieve them + const complexAttrs = { + name: 'test-item', + category: 'electronics', + price: 99.99, + inStock: true, + tags: ['new', 'featured'] + }; + await client.vSetAttr('resp3-key', 'resp3-element', complexAttrs); + + const result = await client.vGetAttr('resp3-key', 'resp3-element'); + + assert.ok(result !== null); + assert.equal(typeof result, 'object') + + assert.deepEqual(result, { + name: 'test-item', + category: 'electronics', + price: 99.99, + inStock: true, + tags: ['new', 'featured'] + }) + + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VGETATTR.ts b/packages/client/lib/commands/VGETATTR.ts new file mode 100644 index 00000000000..05ec8706fb1 --- /dev/null +++ b/packages/client/lib/commands/VGETATTR.ts @@ -0,0 +1,21 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformRedisJsonNullReply } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the attributes of a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve attributes for + * @see https://redis.io/commands/vgetattr/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VGETATTR'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformRedisJsonNullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VINFO.spec.ts b/packages/client/lib/commands/VINFO.spec.ts new file mode 100644 index 00000000000..074598644ff --- /dev/null +++ b/packages/client/lib/commands/VINFO.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VINFO from './VINFO'; +import { BasicCommandParser } from '../client/parser'; + +describe('VINFO', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VINFO.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VINFO', 'key'] + ); + }); + + testUtils.testAll('vInfo', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const result = await client.vInfo('key'); + assert.ok(typeof result === 'object' && result !== null); + + assert.equal(result['vector-dim'], 3); + assert.equal(result['size'], 1); + assert.ok('quant-type' in result); + assert.ok('hnsw-m' in result); + assert.ok('projection-input-dim' in result); + assert.ok('max-level' in result); + assert.ok('attributes-count' in result); + assert.ok('vset-uid' in result); + assert.ok('hnsw-max-node-uid' in result); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vInfo with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + const result = await client.vInfo('resp3-key'); + assert.ok(typeof result === 'object' && result !== null); + + assert.equal(result['vector-dim'], 3); + assert.equal(result['size'], 1); + assert.ok('quant-type' in result); + assert.ok('hnsw-m' in result); + assert.ok('projection-input-dim' in result); + assert.ok('max-level' in result); + assert.ok('attributes-count' in result); + assert.ok('vset-uid' in result); + assert.ok('hnsw-max-node-uid' in result); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VINFO.ts b/packages/client/lib/commands/VINFO.ts new file mode 100644 index 00000000000..4e0d68d7cb0 --- /dev/null +++ b/packages/client/lib/commands/VINFO.ts @@ -0,0 +1,38 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command, UnwrapReply, Resp2Reply, TuplesToMapReply, SimpleStringReply, NumberReply } from '../RESP/types'; + +export type VInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'quant-type'>, SimpleStringReply], + [SimpleStringReply<'vector-dim'>, NumberReply], + [SimpleStringReply<'size'>, NumberReply], + [SimpleStringReply<'max-level'>, NumberReply], + [SimpleStringReply<'vset-uid'>, NumberReply], + [SimpleStringReply<'hnsw-max-node-uid'>, NumberReply], +]>; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve metadata and internal details about a vector set, including size, dimensions, quantization type, and graph structure + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vinfo/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VINFO'); + parser.pushKey(key); + }, + transformReply: { + 2: (reply: UnwrapReply>): VInfoReplyMap => { + 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 VInfoReplyMap; + }, + 3: undefined as unknown as () => VInfoReplyMap + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/VLINKS.spec.ts b/packages/client/lib/commands/VLINKS.spec.ts new file mode 100644 index 00000000000..e788f9f9a98 --- /dev/null +++ b/packages/client/lib/commands/VLINKS.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VLINKS from './VLINKS'; +import { BasicCommandParser } from '../client/parser'; + +describe('VLINKS', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VLINKS.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VLINKS', 'key', 'element'] + ); + }); + + testUtils.testAll('vLinks', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vLinks('key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.length) + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vLinks with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vLinks('resp3-key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.length) + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VLINKS.ts b/packages/client/lib/commands/VLINKS.ts new file mode 100644 index 00000000000..9e97fc7de9b --- /dev/null +++ b/packages/client/lib/commands/VLINKS.ts @@ -0,0 +1,20 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the neighbors of a specified element in a vector set; the connections for each layer of the HNSW graph + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve neighbors for + * @see https://redis.io/commands/vlinks/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VLINKS'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: undefined as unknown as () => ArrayReply> +} as const satisfies Command; diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts new file mode 100644 index 00000000000..db96bd1a8af --- /dev/null +++ b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts @@ -0,0 +1,75 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VLINKS_WITHSCORES from './VLINKS_WITHSCORES'; +import { BasicCommandParser } from '../client/parser'; + +describe('VLINKS WITHSCORES', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VLINKS_WITHSCORES.parseCommand(parser, 'key', 'element'); + assert.deepEqual(parser.redisArgs, [ + 'VLINKS', + 'key', + 'element', + 'WITHSCORES' + ]); + }); + + testUtils.testAll( + 'vLinksWithScores', + async client => { + // Create a vector set with multiple elements to build HNSW graph layers + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('key', [1.2, 2.2, 3.2], 'element3'); + await client.vAdd('key', [2.0, 3.0, 4.0], 'element4'); + + const result = await client.vLinksWithScores('key', 'element1'); + + assert.ok(Array.isArray(result)); + + for (const layer of result) { + assert.equal( + typeof layer, + 'object' + ); + } + + assert.ok(result.length >= 1, 'Should have at least layer 0'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + } + ); + + testUtils.testWithClient( + 'vLinksWithScores with RESP3', + async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [1.2, 2.2, 3.2], 'element3'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element4'); + + const result = await client.vLinksWithScores('resp3-key', 'element1'); + + assert.ok(Array.isArray(result)); + + for (const layer of result) { + assert.equal( + typeof layer, + 'object' + ); + } + + assert.ok(result.length >= 1, 'Should have at least layer 0'); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + } + ); +}); diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.ts new file mode 100644 index 00000000000..10ebe160fcd --- /dev/null +++ b/packages/client/lib/commands/VLINKS_WITHSCORES.ts @@ -0,0 +1,42 @@ +import { BlobStringReply, Command, DoubleReply, MapReply } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VLINKS from './VLINKS'; + + +function transformVLinksWithScoresReply(reply: any): Array> { + const layers: Array> = []; + + for (const layer of reply) { + const obj: Record = Object.create(null); + + // Each layer contains alternating element names and scores + for (let i = 0; i < layer.length; i += 2) { + const element = layer[i]; + const score = transformDoubleReply[2](layer[i + 1]); + obj[element.toString()] = score; + } + + layers.push(obj); + } + + return layers; +} + +export default { + IS_READ_ONLY: VLINKS.IS_READ_ONLY, + /** + * Get the connections for each layer of the HNSW graph with similarity scores + * @param args - Same parameters as the VLINKS command + * @see https://redis.io/commands/vlinks/ + */ + parseCommand(...args: Parameters) { + const parser = args[0]; + + VLINKS.parseCommand(...args); + parser.push('WITHSCORES'); + }, + transformReply: { + 2: transformVLinksWithScoresReply, + 3: undefined as unknown as () => Array> + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/VRANDMEMBER.spec.ts b/packages/client/lib/commands/VRANDMEMBER.spec.ts new file mode 100644 index 00000000000..28c020e3563 --- /dev/null +++ b/packages/client/lib/commands/VRANDMEMBER.spec.ts @@ -0,0 +1,201 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VRANDMEMBER from './VRANDMEMBER'; +import { BasicCommandParser } from '../client/parser'; + +describe('VRANDMEMBER', () => { + describe('parseCommand', () => { + it('without count', () => { + const parser = new BasicCommandParser(); + VRANDMEMBER.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VRANDMEMBER', 'key'] + ); + }); + + it('with count', () => { + const parser = new BasicCommandParser(); + VRANDMEMBER.parseCommand(parser, 'key', 2); + assert.deepEqual( + parser.redisArgs, + ['VRANDMEMBER', 'key', '2'] + ); + }); + }); + + describe('RESP2 tests', () => { + testUtils.testAll('vRandMember without count - returns single element as string', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('key'); + assert.equal(typeof result, 'string'); + assert.ok(['element1', 'element2', 'element3'].includes(result as string)); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember with positive count - returns distinct elements', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('key', 2); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember with negative count - allows duplicates', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('key', -5); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 5); + + // All elements should be from our set (duplicates allowed) + result.forEach(element => { + assert.ok(['element1', 'element2'].includes(element)); + }); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember count exceeds set size - returns entire set', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('key', 10); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); // Only 2 elements exist + + // Should contain both elements + assert.ok(result.includes('element1')); + assert.ok(result.includes('element2')); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember on non-existent key', async client => { + // Without count - should return null + const resultNoCount = await client.vRandMember('nonexistent'); + assert.equal(resultNoCount, null); + + // With count - should return empty array + const resultWithCount = await client.vRandMember('nonexistent', 5); + assert.ok(Array.isArray(resultWithCount)); + assert.equal(resultWithCount.length, 0); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + }); + + describe('RESP3 tests', () => { + testUtils.testWithClient('vRandMember without count - returns single element as string', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('resp3-key'); + assert.equal(typeof result, 'string'); + assert.ok(['element1', 'element2', 'element3'].includes(result as string)); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember with positive count - returns distinct elements', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('resp3-key', 2); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); + + // Should be distinct elements (no duplicates) + const uniqueElements = new Set(result); + assert.equal(uniqueElements.size, 2); + + // All elements should be from our set + result.forEach(element => { + assert.ok(['element1', 'element2', 'element3'].includes(element)); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember with negative count - allows duplicates', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('resp3-key', -5); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 5); + + // All elements should be from our set (duplicates allowed) + result.forEach(element => { + assert.ok(['element1', 'element2'].includes(element)); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember count exceeds set size - returns entire set', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('resp3-key', 10); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); // Only 2 elements exist + + // Should contain both elements + assert.ok(result.includes('element1')); + assert.ok(result.includes('element2')); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember on non-existent key', async client => { + // Without count - should return null + const resultNoCount = await client.vRandMember('resp3-nonexistent'); + assert.equal(resultNoCount, null); + + // With count - should return empty array + const resultWithCount = await client.vRandMember('resp3-nonexistent', 5); + assert.ok(Array.isArray(resultWithCount)); + assert.equal(resultWithCount.length, 0); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + }); +}); diff --git a/packages/client/lib/commands/VRANDMEMBER.ts b/packages/client/lib/commands/VRANDMEMBER.ts new file mode 100644 index 00000000000..299af33b9fa --- /dev/null +++ b/packages/client/lib/commands/VRANDMEMBER.ts @@ -0,0 +1,23 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, ArrayReply, Command, NullReply } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve random elements of a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param count - Optional number of elements to return + * @see https://redis.io/commands/vrandmember/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, count?: number) { + parser.push('VRANDMEMBER'); + parser.pushKey(key); + + if (count !== undefined) { + parser.push(count.toString()); + } + }, + transformReply: undefined as unknown as () => BlobStringReply | ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VREM.spec.ts b/packages/client/lib/commands/VREM.spec.ts new file mode 100644 index 00000000000..9e558c991c1 --- /dev/null +++ b/packages/client/lib/commands/VREM.spec.ts @@ -0,0 +1,63 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VREM from './VREM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VREM', () => { + const parser = new BasicCommandParser(); + VREM.parseCommand(parser, 'key', 'element'); + it('parseCommand', () => { + assert.deepEqual( + parser.redisArgs, + ['VREM', 'key', 'element'] + ); + }); + + testUtils.testAll('vRem', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vRem('key', 'element'), + true + ); + + assert.equal( + await client.vRem('key', 'element'), + false + ); + + assert.equal( + await client.vCard('key'), + 0 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vRem with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + assert.equal( + await client.vRem('resp3-key', 'resp3-element'), + true + ); + + assert.equal( + await client.vRem('resp3-key', 'resp3-element'), + false + ); + + + assert.equal( + await client.vCard('resp3-key'), + 0 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VREM.ts b/packages/client/lib/commands/VREM.ts new file mode 100644 index 00000000000..7eb22b2e2ec --- /dev/null +++ b/packages/client/lib/commands/VREM.ts @@ -0,0 +1,20 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply } from './generic-transformers'; + +export default { + /** + * Remove an element from a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to remove from the vector set + * @see https://redis.io/commands/vrem/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VREM'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSETATTR.spec.ts b/packages/client/lib/commands/VSETATTR.spec.ts new file mode 100644 index 00000000000..303006d4081 --- /dev/null +++ b/packages/client/lib/commands/VSETATTR.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSETATTR from './VSETATTR'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSETATTR', () => { + describe('parseCommand', () => { + it('with object', () => { + const parser = new BasicCommandParser(); + VSETATTR.parseCommand(parser, 'key', 'element', { name: 'test', value: 42 }), + assert.deepEqual( + parser.redisArgs, + ['VSETATTR', 'key', 'element', '{"name":"test","value":42}'] + ); + }); + + it('with string', () => { + const parser = new BasicCommandParser(); + VSETATTR.parseCommand(parser, 'key', 'element', '{"name":"test"}'), + assert.deepEqual( + parser.redisArgs, + ['VSETATTR', 'key', 'element', '{"name":"test"}'] + ); + }); + }); + + testUtils.testAll('vSetAttr', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vSetAttr('key', 'element', { name: 'test', value: 42 }), + true + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vSetAttr with RESP3 - returns boolean', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + const result = await client.vSetAttr('resp3-key', 'resp3-element', { + name: 'test-item', + category: 'electronics', + price: 99.99 + }); + + // RESP3 returns boolean instead of number + assert.equal(typeof result, 'boolean'); + assert.equal(result, true); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VSETATTR.ts b/packages/client/lib/commands/VSETATTR.ts new file mode 100644 index 00000000000..084b8f8008e --- /dev/null +++ b/packages/client/lib/commands/VSETATTR.ts @@ -0,0 +1,32 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply } from './generic-transformers'; + +export default { + /** + * Set or replace attributes on a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to set attributes for + * @param attributes - The attributes to set (as JSON string or object) + * @see https://redis.io/commands/vsetattr/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + element: RedisArgument, + attributes: RedisArgument | Record + ) { + parser.push('VSETATTR'); + parser.pushKey(key); + parser.push(element); + + if (typeof attributes === 'object' && attributes !== null) { + parser.push(JSON.stringify(attributes)); + } else { + parser.push(attributes); + } + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSIM.spec.ts b/packages/client/lib/commands/VSIM.spec.ts new file mode 100644 index 00000000000..b7e10eb6c49 --- /dev/null +++ b/packages/client/lib/commands/VSIM.spec.ts @@ -0,0 +1,85 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSIM from './VSIM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSIM', () => { + describe('parseCommand', () => { + it('with vector', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', [1.0, 2.0, 3.0]), + assert.deepEqual( + parser.redisArgs, + ['VSIM', 'key', 'VALUES', '3', '1', '2', '3'] + ); + }); + + it('with element', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VSIM', 'key', 'ELE', 'element'] + ); + }); + + it('with options', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', 'element', { + COUNT: 5, + EF: 100, + FILTER: '.price > 20', + 'FILTER-EF': 50, + TRUTH: true, + NOTHREAD: true + }); + assert.deepEqual( + parser.redisArgs, + [ + 'VSIM', 'key', 'ELE', 'element', + 'COUNT', '5', 'EF', '100', 'FILTER', '.price > 20', + 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD' + ] + ); + }); + }); + + testUtils.testAll('vSim', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSim('key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.includes('element1')); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vSim with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3'); + + // Test similarity search with vector + const resultWithVector = await client.vSim('resp3-key', [1.05, 2.05, 3.05]); + assert.ok(Array.isArray(resultWithVector)); + assert.ok(resultWithVector.length > 0); + + // Test similarity search with element + const resultWithElement = await client.vSim('resp3-key', 'element1'); + assert.ok(Array.isArray(resultWithElement)); + assert.ok(resultWithElement.includes('element1')); + + // Test with options + const resultWithOptions = await client.vSim('resp3-key', 'element1', { COUNT: 2 }); + assert.ok(Array.isArray(resultWithOptions)); + assert.ok(resultWithOptions.length <= 2); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts new file mode 100644 index 00000000000..dc41a54caf1 --- /dev/null +++ b/packages/client/lib/commands/VSIM.ts @@ -0,0 +1,68 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformDoubleArgument } from './generic-transformers'; + +export interface VSimOptions { + COUNT?: number; + EF?: number; + FILTER?: string; + 'FILTER-EF'?: number; + TRUTH?: boolean; + NOTHREAD?: boolean; +} + +export default { + IS_READ_ONLY: true, + /** + * Retrieve elements similar to a given vector or element with optional filtering + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param query - The query vector (array of numbers) or element name (string) + * @param options - Optional parameters for similarity search + * @see https://redis.io/commands/vsim/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + query: RedisArgument | Array, + options?: VSimOptions + ) { + parser.push('VSIM'); + parser.pushKey(key); + + if (Array.isArray(query)) { + parser.push('VALUES', query.length.toString()); + for (const value of query) { + parser.push(transformDoubleArgument(value)); + } + } else { + parser.push('ELE', query); + } + + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); + } + + if (options?.EF !== undefined) { + parser.push('EF', options.EF.toString()); + } + + if (options?.FILTER) { + parser.push('FILTER', options.FILTER); + } + + if (options?.['FILTER-EF'] !== undefined) { + parser.push('FILTER-EF', options['FILTER-EF'].toString()); + } + + if (options?.TRUTH) { + parser.push('TRUTH'); + } + + if (options?.NOTHREAD) { + parser.push('NOTHREAD'); + } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts new file mode 100644 index 00000000000..ff9bc41376f --- /dev/null +++ b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts @@ -0,0 +1,62 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSIM_WITHSCORES from './VSIM_WITHSCORES'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSIM WITHSCORES', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VSIM_WITHSCORES.parseCommand(parser, 'key', 'element') + assert.deepEqual(parser.redisArgs, [ + 'VSIM', + 'key', + 'ELE', + 'element', + 'WITHSCORES' + ]); + }); + + testUtils.testAll( + 'vSimWithScores', + async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSimWithScores('key', 'element1'); + + assert.ok(typeof result === 'object'); + assert.ok('element1' in result); + assert.ok('element2' in result); + assert.equal(typeof result['element1'], 'number'); + assert.equal(typeof result['element2'], 'number'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + } + ); + + testUtils.testWithClient( + 'vSimWithScores with RESP3 - returns Map with scores', + async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3'); + + const result = await client.vSimWithScores('resp3-key', 'element1'); + + assert.ok(typeof result === 'object'); + assert.ok('element1' in result); + assert.ok('element2' in result); + assert.equal(typeof result['element1'], 'number'); + assert.equal(typeof result['element2'], 'number'); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + } + ); +}); diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.ts b/packages/client/lib/commands/VSIM_WITHSCORES.ts new file mode 100644 index 00000000000..fda05be6642 --- /dev/null +++ b/packages/client/lib/commands/VSIM_WITHSCORES.ts @@ -0,0 +1,36 @@ +import { + ArrayReply, + BlobStringReply, + Command, + DoubleReply, + MapReply, + UnwrapReply +} from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VSIM from './VSIM'; + +export default { + IS_READ_ONLY: VSIM.IS_READ_ONLY, + /** + * Retrieve elements similar to a given vector or element with similarity scores + * @param args - Same parameters as the VSIM command + * @see https://redis.io/commands/vsim/ + */ + parseCommand(...args: Parameters) { + const parser = args[0]; + + VSIM.parseCommand(...args); + parser.push('WITHSCORES'); + }, + transformReply: { + 2: (reply: ArrayReply) => { + const inferred = reply as unknown as UnwrapReply; + const members: Record = {}; + for (let i = 0; i < inferred.length; i += 2) { + members[inferred[i].toString()] = transformDoubleReply[2](inferred[i + 1]); + } + return members; + }, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index 91eab7107a1..022339e4bb7 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -662,3 +662,21 @@ export function transformStreamsMessagesReplyResp3(reply: UnwrapReply } } + +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); +} + +export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { + const res = JSON.parse((json as unknown as UnwrapReply).toString()); + return res; +} + +export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { + return isNullReply(json) ? json : transformRedisJsonReply(json); +} diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 5cd81331a4e..87ab8d10b8f 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -344,6 +344,20 @@ import ZSCORE from './ZSCORE'; import ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; import ZUNION from './ZUNION'; import ZUNIONSTORE from './ZUNIONSTORE'; +import VADD from './VADD'; +import VCARD from './VCARD'; +import VDIM from './VDIM'; +import VEMB from './VEMB'; +import VEMB_RAW from './VEMB_RAW'; +import VGETATTR from './VGETATTR'; +import VINFO from './VINFO'; +import VLINKS from './VLINKS'; +import VLINKS_WITHSCORES from './VLINKS_WITHSCORES'; +import VRANDMEMBER from './VRANDMEMBER'; +import VREM from './VREM'; +import VSETATTR from './VSETATTR'; +import VSIM from './VSIM'; +import VSIM_WITHSCORES from './VSIM_WITHSCORES'; export default { ACL_CAT, @@ -1037,5 +1051,33 @@ export default { ZUNION, zUnion: ZUNION, ZUNIONSTORE, - zUnionStore: ZUNIONSTORE + zUnionStore: ZUNIONSTORE, + VADD, + vAdd: VADD, + VCARD, + vCard: VCARD, + VDIM, + vDim: VDIM, + VEMB, + vEmb: VEMB, + VEMB_RAW, + vEmbRaw: VEMB_RAW, + VGETATTR, + vGetAttr: VGETATTR, + VINFO, + vInfo: VINFO, + VLINKS, + vLinks: VLINKS, + VLINKS_WITHSCORES, + vLinksWithScores: VLINKS_WITHSCORES, + VRANDMEMBER, + vRandMember: VRANDMEMBER, + VREM, + vRem: VREM, + VSETATTR, + vSetAttr: VSETATTR, + VSIM, + vSim: VSIM, + VSIM_WITHSCORES, + vSimWithScores: VSIM_WITHSCORES } as const satisfies RedisCommands; diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index d1082baf48e..b98d1532b4e 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,5 +1,5 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 69485f55a6c..1437fab4d56 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonArrIndexOptions { range?: { diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index 33fe30a99e8..7a5ab945892 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 53d9ed2dc87..88e4da96980 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,7 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; 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 './helpers'; +import { isArrayReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface RedisArrPopOptions { path: RedisArgument; diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index e514fefae39..14ec46a53af 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,7 +1,6 @@ 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 { transformRedisJsonNullReply } from './helpers'; +import { RedisVariadicArgument, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonGetOptions { path?: RedisVariadicArgument; diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 72baea1048a..1a4b54fc4ba 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index 7bb948bc667..01a7783b922 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformRedisJsonNullReply } from './helpers'; +import { transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index 9e5ec1799f1..81e8d4c6bdf 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonMSetItem { key: RedisArgument; diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index a0df41fa89d..9ab680b4898 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,6 +1,6 @@ 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 './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonSetOptions { condition?: 'NX' | 'XX'; diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index aa8f3772fb1..b3115f684c3 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,6 +1,6 @@ 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 './helpers'; +import { transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonStrAppendOptions { path?: RedisArgument; diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts deleted file mode 100644 index 99579ce81cb..00000000000 --- a/packages/json/lib/commands/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformers"; -import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; - -export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - return isNullReply(json) ? json : transformRedisJsonReply(json); -} - -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); -} - -export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { - const res = JSON.parse((json as unknown as UnwrapReply).toString()); - return res; -} diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index a9e16bde757..0e29bdd648d 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -23,7 +23,9 @@ import STRLEN from './STRLEN'; import TOGGLE from './TOGGLE'; import TYPE from './TYPE'; -export * from './helpers'; +// Re-export helper types and functions from client package +export type { RedisJSON } from '@redis/client/dist/lib/commands/generic-transformers'; +export { transformRedisJsonArgument, transformRedisJsonReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { ARRAPPEND, From 742d5713e8938be0f3b93adb2ddc858edf196ff4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 25 Jun 2025 13:15:44 +0300 Subject: [PATCH 174/244] fix(commands): sPopCount return Array (#3006) Also, touch the tests for spop and spopcount to use the new parseCommand API fixes #3004 --- packages/client/lib/commands/SPOP.spec.ts | 19 +++++++++++++++++-- .../client/lib/commands/SPOP_COUNT.spec.ts | 19 +++++++++++++++++-- packages/client/lib/commands/SPOP_COUNT.ts | 4 ++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/client/lib/commands/SPOP.spec.ts b/packages/client/lib/commands/SPOP.spec.ts index f435134416b..542e1ba3fcb 100644 --- a/packages/client/lib/commands/SPOP.spec.ts +++ b/packages/client/lib/commands/SPOP.spec.ts @@ -1,12 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP from './SPOP'; -import { parseArgs } from './generic-transformers'; +import { BasicCommandParser } from '../client/parser'; describe('SPOP', () => { it('transformArguments', () => { + const parser = new BasicCommandParser(); + SPOP.parseCommand(parser, 'key'); assert.deepEqual( - parseArgs(SPOP, 'key'), + parser.redisArgs, ['SPOP', 'key'] ); }); @@ -16,6 +18,19 @@ describe('SPOP', () => { await client.sPop('key'), null ); + + await client.sAdd('key', 'member'); + + assert.equal( + await client.sPop('key'), + 'member' + ); + + assert.equal( + await client.sPop('key'), + null + ); + }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN diff --git a/packages/client/lib/commands/SPOP_COUNT.spec.ts b/packages/client/lib/commands/SPOP_COUNT.spec.ts index 935ff437800..9720101f31f 100644 --- a/packages/client/lib/commands/SPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts @@ -1,21 +1,36 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP_COUNT from './SPOP_COUNT'; -import { parseArgs } from './generic-transformers'; +import { BasicCommandParser } from '../client/parser'; describe('SPOP_COUNT', () => { it('transformArguments', () => { + const parser = new BasicCommandParser(); + SPOP_COUNT.parseCommand(parser, 'key', 1); assert.deepEqual( - parseArgs(SPOP_COUNT, 'key', 1), + parser.redisArgs, ['SPOP', 'key', '1'] ); }); testUtils.testAll('sPopCount', async client => { + assert.deepEqual( await client.sPopCount('key', 1), [] ); + + await Promise.all([ + client.sAdd('key', 'member'), + client.sAdd('key', 'member2'), + client.sAdd('key', 'member3') + ]) + + assert.deepEqual( + (await client.sPopCount('key', 3)).length, + 3 + ); + }, { 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 index 1191f07cff2..a285e6f5c48 100644 --- a/packages/client/lib/commands/SPOP_COUNT.ts +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -1,5 +1,5 @@ import { CommandParser } from '../client/parser'; -import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { RedisArgument, Command, ArrayReply } from '../RESP/types'; export default { IS_READ_ONLY: false, @@ -16,5 +16,5 @@ export default { parser.pushKey(key); parser.push(count.toString()); }, - transformReply: undefined as unknown as () => BlobStringReply | NullReply + transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; From 6f3380ba68786a3d5a9825f9a29c2053e43d32e3 Mon Sep 17 00:00:00 2001 From: Ricardo Ferreira Date: Wed, 25 Jun 2025 13:41:54 +0100 Subject: [PATCH 175/244] fix: ensure the repo links in the README are functional on the website (#3005) --- README.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 38615ee519e..ab6b4707e6f 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ npm install redis | 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/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 | +| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/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)! @@ -83,7 +83,7 @@ createClient({ ``` 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). +the [client configuration guide](https://github.com/redis/node-redis/blob/master/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 @@ -188,7 +188,7 @@ await pool.ping(); ### Pub/Sub -See the [Pub/Sub overview](./docs/pub-sub.md). +See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md). ### Scan Iterator @@ -234,7 +234,6 @@ of sending a `QUIT` command to the server, the client can simply close the netwo ```typescript client.destroy(); ``` - ### Client Side Caching Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid. @@ -251,7 +250,7 @@ const client = createClient({ }); ``` -See the [V5 documentation](./docs/v5.md#client-side-caching) for more details and advanced usage. +See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining @@ -275,11 +274,11 @@ await Promise.all([ ### Programmability -See the [Programmability overview](./docs/programmability.md). +See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md). ### Clustering -Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster. +Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster. ### Events @@ -292,12 +291,12 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `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) | +| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | See [here](https://github.com/redis/node-redis/blob/master/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. +> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions @@ -314,13 +313,13 @@ Node Redis is supported with the following versions of Redis: ## Migration -- [From V3 to V4](docs/v3-to-v4.md) -- [From V4 to V5](docs/v4-to-v5.md) -- [V5](docs/v5.md) +- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md) +- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md) +- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md) ## Contributing -If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md). Thank you to all the people who already contributed to Node Redis! @@ -328,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis! ## License -This repository is licensed under the "MIT" license. See [LICENSE](LICENSE). +This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE). From 79749f24618baa7d977470372cafaa4b747f0837 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 7 Jul 2025 11:06:34 +0300 Subject: [PATCH 176/244] fix(sentinel): propagate RESP option to clients (#3011) `createSentinel` takes RESP as an option, but does not propagate down to the actual clients. This creates confusion for the users as they expect the option to be set to all clients, which is reasonable. In case of clientSideCaching, this problem manifests as validation failure because clientSideCaching requires RESP3, but if we dont propagate, clients start with the default RESP2 fixes #3010 --- packages/client/lib/sentinel/index.spec.ts | 23 ++++++++++++++-------- packages/client/lib/sentinel/index.ts | 5 +++++ packages/test-utils/lib/index.ts | 2 ++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a9bdc8cc95c..ef1702eab13 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -23,7 +23,7 @@ describe('RedisSentinel', () => { { host: 'localhost', port: 26379 } ] }; - + it('should throw error when clientSideCache is enabled with RESP 2', () => { assert.throws( () => RedisSentinel.create({ @@ -46,7 +46,7 @@ describe('RedisSentinel', () => { }); it('should not throw when clientSideCache is enabled with RESP 3', () => { - assert.doesNotThrow(() => + assert.doesNotThrow(() => RedisSentinel.create({ ...options, clientSideCache: clientSideCacheConfig, @@ -54,6 +54,16 @@ describe('RedisSentinel', () => { }) ); }); + + testUtils.testWithClientSentinel('should successfully connect to sentinel', async () => { + }, { + ...GLOBAL.SENTINEL.OPEN, + sentinelOptions: { + RESP: 3, + clientSideCache: { ttl: 0, maxEntries: 0, evictPolicy: 'LRU'}, + }, + }) + }); }); }); @@ -417,7 +427,7 @@ async function steadyState(frame: SentinelFramework) { sentinel.setTracer(tracer); await sentinel.connect(); await nodePromise; - + await sentinel.flushAll(); } finally { if (sentinel !== undefined) { @@ -443,7 +453,7 @@ describe('legacy tests', () => { this.timeout(15000); last = Date.now(); - + function deltaMeasurer() { const delta = Date.now() - last; if (delta > longestDelta) { @@ -508,7 +518,7 @@ describe('legacy tests', () => { } stopMeasuringBlocking = true; - + await frame.cleanup(); }) @@ -1032,6 +1042,3 @@ describe('legacy tests', () => { }) }); }); - - - diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index ec570e64bf2..b4a794b871a 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -625,6 +625,7 @@ class RedisSentinelInternal< readonly #sentinelClientOptions: RedisClientOptions; readonly #scanInterval: number; readonly #passthroughClientErrorEvents: boolean; + readonly #RESP?: RespVersions; #anotherReset = false; @@ -673,6 +674,7 @@ class RedisSentinelInternal< this.#name = options.name; + this.#RESP = options.RESP; this.#sentinelRootNodes = Array.from(options.sentinelRootNodes); this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16; this.#masterPoolSize = options.masterPoolSize ?? 1; @@ -716,6 +718,9 @@ class RedisSentinelInternal< #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) { return RedisClient.create({ + //first take the globally set RESP + RESP: this.#RESP, + //then take the client options, which can in theory overwrite it ...clientOptions, socket: { ...clientOptions.socket, diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index a41f970e0c8..43dd4debfdf 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -337,6 +337,7 @@ export default class TestUtils { port: promise.port })); + const sentinel = createSentinel({ name: 'mymaster', sentinelRootNodes: rootNodes, @@ -352,6 +353,7 @@ export default class TestUtils { functions: options?.functions || {}, masterPoolSize: options?.masterPoolSize || undefined, reserveClient: options?.reserveClient || false, + ...options?.sentinelOptions }) as RedisSentinelType; if (options.disableClientSetup) { From 65a12d50e711d86c6172f586b6a82c33efaf4bb2 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 7 Jul 2025 11:37:08 +0300 Subject: [PATCH 177/244] feat(client): add command timeout option (#3008) Co-authored-by: Florian Schunk <149071178+florian-schunk@users.noreply.github.com> --- docs/command-options.md | 13 ++ packages/client/lib/client/commands-queue.ts | 56 +++++++-- packages/client/lib/client/index.spec.ts | 118 +++++++++++++++---- packages/client/lib/client/index.ts | 12 +- packages/client/lib/cluster/index.ts | 28 +++-- packages/client/lib/sentinel/index.ts | 38 +++--- packages/client/lib/sentinel/utils.ts | 2 +- packages/test-utils/lib/index.ts | 21 ++-- 8 files changed, 212 insertions(+), 76 deletions(-) diff --git a/docs/command-options.md b/docs/command-options.md index b246445ad74..8583eae135b 100644 --- a/docs/command-options.md +++ b/docs/command-options.md @@ -37,6 +37,19 @@ try { } ``` + +## Timeout + +This option is similar to the Abort Signal one, but provides an easier way to set timeout for commands. Again, this applies to commands that haven't been written to the socket yet. + +```javascript +const client = createClient({ + commandOptions: { + timeout: 1000 + } +}) +``` + ## ASAP Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue. diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index 78c0a01b203..52a07a7e3b5 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -3,7 +3,7 @@ import encodeCommand from '../RESP/encoder'; import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder'; import { TypeMapping, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types'; import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub'; -import { AbortError, ErrorReply } from '../errors'; +import { AbortError, ErrorReply, TimeoutError } from '../errors'; import { MonitorCallback } from '.'; export interface CommandOptions { @@ -14,6 +14,10 @@ export interface CommandOptions { * Maps between RESP and JavaScript types */ typeMapping?: T; + /** + * Timeout for the command in milliseconds + */ + timeout?: number; } export interface CommandToWrite extends CommandWaitingForReply { @@ -23,6 +27,10 @@ export interface CommandToWrite extends CommandWaitingForReply { signal: AbortSignal; listener: () => unknown; } | undefined; + timeout: { + signal: AbortSignal; + listener: () => unknown; + } | undefined; } interface CommandWaitingForReply { @@ -80,7 +88,7 @@ export default class RedisCommandsQueue { #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(); @@ -153,12 +161,26 @@ export default class RedisCommandsQueue { args, chainId: options?.chainId, abort: undefined, + timeout: undefined, resolve, reject, channelsCounter: undefined, typeMapping: options?.typeMapping }; + const timeout = options?.timeout; + if (timeout) { + const signal = AbortSignal.timeout(timeout); + value.timeout = { + signal, + listener: () => { + this.#toWrite.remove(node); + value.reject(new TimeoutError()); + } + }; + signal.addEventListener('abort', value.timeout.listener, { once: true }); + } + const signal = options?.abortSignal; if (signal) { value.abort = { @@ -181,6 +203,7 @@ export default class RedisCommandsQueue { args: command.args, chainId, abort: undefined, + timeout: undefined, resolve() { command.resolve(); resolve(); @@ -202,7 +225,7 @@ export default class RedisCommandsQueue { 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; @@ -250,7 +273,7 @@ export default class RedisCommandsQueue { if (!this.#pubSub.isActive) { this.#resetDecoderCallbacks(); } - + resolve(); }; } @@ -299,6 +322,7 @@ export default class RedisCommandsQueue { args: ['MONITOR'], chainId: options?.chainId, abort: undefined, + timeout: 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 @@ -317,7 +341,7 @@ export default class RedisCommandsQueue { reject, channelsCounter: undefined, typeMapping - }, options?.asap); + }, options?.asap); }); } @@ -340,11 +364,11 @@ export default class RedisCommandsQueue { this.#resetDecoderCallbacks(); this.#resetFallbackOnReply = undefined; this.#pubSub.reset(); - + this.#waitingForReply.shift()!.resolve(reply); return; } - + this.#resetFallbackOnReply!(reply); }) as Decoder['onReply']; @@ -352,6 +376,7 @@ export default class RedisCommandsQueue { args: ['RESET'], chainId, abort: undefined, + timeout: undefined, resolve, reject, channelsCounter: undefined, @@ -376,16 +401,20 @@ export default class RedisCommandsQueue { continue; } - // TODO reuse `toSend` or create new object? + // TODO reuse `toSend` or create new object? (toSend as any).args = undefined; if (toSend.abort) { RedisCommandsQueue.#removeAbortListener(toSend); toSend.abort = undefined; } + if (toSend.timeout) { + RedisCommandsQueue.#removeTimeoutListener(toSend); + toSend.timeout = undefined; + } this.#chainInExecution = toSend.chainId; toSend.chainId = undefined; this.#waitingForReply.push(toSend); - + yield encoded; toSend = this.#toWrite.shift(); } @@ -402,11 +431,18 @@ export default class RedisCommandsQueue { command.abort!.signal.removeEventListener('abort', command.abort!.listener); } + static #removeTimeoutListener(command: CommandToWrite) { + command.timeout!.signal.removeEventListener('abort', command.timeout!.listener); + } + static #flushToWrite(toBeSent: CommandToWrite, err: Error) { if (toBeSent.abort) { RedisCommandsQueue.#removeAbortListener(toBeSent); } - + if (toBeSent.timeout) { + RedisCommandsQueue.#removeTimeoutListener(toBeSent); + } + toBeSent.reject(err); } diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 4f752210dbe..f04d6467062 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1,9 +1,9 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisClient, { RedisClientOptions, RedisClientType } from '.'; -import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors'; +import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, TimeoutError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; -import { spy } from 'sinon'; +import { spy, stub } from 'sinon'; import { once } from 'node:events'; import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec'; import { RESP_TYPES } from '../RESP/decoder'; @@ -239,30 +239,84 @@ describe('Client', () => { assert.equal(await client.sendCommand(['PING']), 'PONG'); }, GLOBAL.SERVERS.OPEN); - describe('AbortController', () => { - before(function () { - if (!global.AbortController) { - this.skip(); - } + testUtils.testWithClient('Unactivated AbortController should not abort', async client => { + await client.sendCommand(['PING'], { + abortSignal: new AbortController().signal }); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('success', async client => { - await client.sendCommand(['PING'], { - abortSignal: new AbortController().signal - }); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('AbortError', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.sendCommand(['PING'], { + abortSignal: AbortSignal.timeout(5) + }), AbortError); + }) + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('AbortError', client => { - const controller = new AbortController(); - controller.abort(); + testUtils.testWithClient('Timeout with custom timeout config', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.sendCommand(['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.SERVERS.OPEN); - return assert.rejects( - client.sendCommand(['PING'], { - abortSignal: controller.signal - }), - AbortError - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithCluster('Timeout with custom timeout config (cluster)', async cluster => { + await blockSetImmediate(async () => { + await assert.rejects(cluster.sendCommand(undefined, true, ['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithClientSentinel('Timeout with custom timeout config (sentinel)', async sentinel => { + await blockSetImmediate(async () => { + await assert.rejects(sentinel.sendCommand(true, ['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithClient('Timeout with global timeout config', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.ping(), TimeoutError); + await assert.rejects(client.sendCommand(['PING']), TimeoutError); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + commandOptions: { + timeout: 5 + } + } + }); + + testUtils.testWithCluster('Timeout with global timeout config (cluster)', async cluster => { + await blockSetImmediate(async () => { + await assert.rejects(cluster.HSET('key', 'foo', 'value'), TimeoutError); + await assert.rejects(cluster.sendCommand(undefined, true, ['PING']), TimeoutError); + }); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + commandOptions: { + timeout: 5 + } + } + }); + + testUtils.testWithClientSentinel('Timeout with global timeout config (sentinel)', async sentinel => { + await blockSetImmediate(async () => { + await assert.rejects(sentinel.HSET('key', 'foo', 'value'), TimeoutError); + await assert.rejects(sentinel.sendCommand(true, ['PING']), TimeoutError); + }); + }, { + ...GLOBAL.SENTINEL.OPEN, + clientOptions: { + commandOptions: { + timeout: 5 + } + } }); testUtils.testWithClient('undefined and null should not break the client', async client => { @@ -900,3 +954,23 @@ describe('Client', () => { }, GLOBAL.SERVERS.OPEN); }); }); + +/** + * Executes the provided function in a context where setImmediate is stubbed to not do anything. + * This blocks setImmediate callbacks from executing + */ +async function blockSetImmediate(fn: () => Promise) { + let setImmediateStub: any; + + try { + setImmediateStub = stub(global, 'setImmediate'); + setImmediateStub.callsFake(() => { + //Dont call the callback, effectively blocking execution + }); + await fn(); + } finally { + if (setImmediateStub) { + setImmediateStub.restore(); + } + } +} diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index a446ad8e755..128dc599677 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -526,7 +526,7 @@ export default class RedisClient< async #handshake(chainId: symbol, asap: boolean) { const promises = []; const commandsWithErrorHandlers = await this.#getHandshakeCommands(); - + if (asap) commandsWithErrorHandlers.reverse() for (const { cmd, errorHandler } of commandsWithErrorHandlers) { @@ -632,7 +632,7 @@ export default class RedisClient< // since they could be connected to an older version that doesn't support them. } }); - + commands.push({ cmd: [ 'CLIENT', @@ -889,7 +889,13 @@ export default class RedisClient< return Promise.reject(new ClientOfflineError()); } - const promise = this._self.#queue.addCommand(args, options); + // Merge global options with provided options + const opts = { + ...this._self._commandOptions, + ...options + } + + const promise = this._self.#queue.addCommand(args, opts); this._self.#scheduleWrite(); return promise; } diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index c2c251810e3..6d26ac98c9a 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -38,12 +38,12 @@ export interface RedisClusterOptions< // POLICIES extends CommandPolicies = CommandPolicies > extends ClusterCommander { /** - * Should contain details for some of the cluster nodes that the client will use to discover + * 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, + * 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; @@ -68,13 +68,13 @@ export interface RedisClusterOptions< nodeAddressMap?: NodeAddressMap; /** * Client Side Caching configuration for the pool. - * - * Enables Redis Servers and Clients to work together to cache results from commands + * + * Enables Redis Servers and Clients to work together to cache results from commands * sent to a server. The server will notify the client when cached results are no longer valid. * In pooled mode, the cache is shared across all clients in the pool. - * + * * Note: Client Side Caching is only supported with RESP3. - * + * * @example Anonymous cache configuration * ``` * const client = createCluster({ @@ -86,7 +86,7 @@ export interface RedisClusterOptions< * minimum: 5 * }); * ``` - * + * * @example Using a controllable cache * ``` * const cache = new BasicPooledClientSideCache({ @@ -406,7 +406,7 @@ export default class RedisCluster< proxy._commandOptions[key] = value; return proxy as RedisClusterType< M, - F, + F, S, RESP, K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING @@ -489,7 +489,7 @@ export default class RedisCluster< myFn = this._handleAsk(fn); continue; } - + if (err.message.startsWith('MOVED')) { await this._slots.rediscover(client); client = await this._slots.getClient(firstKey, isReadonly); @@ -497,7 +497,7 @@ export default class RedisCluster< } throw err; - } + } } } @@ -508,10 +508,16 @@ export default class RedisCluster< options?: ClusterCommandOptions, // defaultPolicies?: CommandPolicies ): Promise { + + // Merge global options with local options + const opts = { + ...this._self._commandOptions, + ...options + } return this._self._execute( firstKey, isReadonly, - options, + opts, (client, opts) => client.sendCommand(args, opts) ); } diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index b4a794b871a..b3f3bbf0b8d 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -35,7 +35,7 @@ export class RedisSentinelClient< /** * Indicates if the client connection is open - * + * * @returns `true` if the client connection is open, `false` otherwise */ @@ -45,7 +45,7 @@ export class RedisSentinelClient< /** * Indicates if the client connection is ready to accept commands - * + * * @returns `true` if the client connection is ready, `false` otherwise */ get isReady() { @@ -54,7 +54,7 @@ export class RedisSentinelClient< /** * Gets the command options configured for this client - * + * * @returns The command options for this client or `undefined` if none were set */ get commandOptions() { @@ -241,10 +241,10 @@ export class RedisSentinelClient< /** * 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 @@ -274,7 +274,7 @@ export default class RedisSentinel< /** * Indicates if the sentinel connection is open - * + * * @returns `true` if the sentinel connection is open, `false` otherwise */ get isOpen() { @@ -283,7 +283,7 @@ export default class RedisSentinel< /** * Indicates if the sentinel connection is ready to accept commands - * + * * @returns `true` if the sentinel connection is ready, `false` otherwise */ get isReady() { @@ -554,15 +554,15 @@ export default class RedisSentinel< /** * 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() @@ -671,7 +671,7 @@ class RedisSentinelInternal< super(); this.#validateOptions(options); - + this.#name = options.name; this.#RESP = options.RESP; @@ -733,7 +733,7 @@ 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 */ @@ -748,10 +748,10 @@ class RedisSentinelInternal< /** * 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 @@ -791,10 +791,10 @@ class RedisSentinelInternal< async #connect() { let count = 0; - while (true) { + while (true) { this.#trace("starting connect loop"); - count+=1; + count+=1; if (this.#destroy) { this.#trace("in #connect and want to destroy") return; @@ -847,7 +847,7 @@ class RedisSentinelInternal< try { /* - // force testing of READONLY errors + // force testing of READONLY errors if (clientInfo !== undefined) { if (Math.floor(Math.random() * 10) < 1) { console.log("throwing READONLY error"); @@ -861,7 +861,7 @@ class RedisSentinelInternal< 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 @@ -1574,4 +1574,4 @@ export class RedisSentinelFactory extends EventEmitter { } }); } -} \ No newline at end of file +} diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index 7e2404c2f7a..c124981e257 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -6,7 +6,7 @@ import { NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, Pr /* 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; } diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 43dd4debfdf..aab1c700f5e 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -179,7 +179,7 @@ export default class TestUtils { this.#VERSION_NUMBERS = numbers; this.#DOCKER_IMAGE = { image: dockerImageName, - version: string, + version: string, mode: "server" }; } @@ -315,7 +315,7 @@ export default class TestUtils { if (passIndex != 0) { password = options.serverArguments[passIndex]; } - + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { const dockerImage = this.#DOCKER_IMAGE; before(function () { @@ -333,18 +333,19 @@ export default class TestUtils { const promises = await dockerPromises; const rootNodes: Array = promises.map(promise => ({ - host: "127.0.0.1", + host: "127.0.0.1", port: promise.port })); const sentinel = createSentinel({ - name: 'mymaster', - sentinelRootNodes: rootNodes, - nodeClientOptions: { + name: 'mymaster', + sentinelRootNodes: rootNodes, + nodeClientOptions: { + commandOptions: options.clientOptions?.commandOptions, password: password || undefined, }, - sentinelClientOptions: { + sentinelClientOptions: { password: password || undefined, }, replicaPoolSize: options?.replicaPoolSize || 0, @@ -507,7 +508,7 @@ export default class TestUtils { it(title, async function () { if (!dockersPromise) return this.skip(); - + const dockers = await dockersPromise, cluster = createCluster({ rootNodes: dockers.map(({ port }) => ({ @@ -580,12 +581,12 @@ export default class TestUtils { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir)) - + if (tmpDir) { fs.rmSync(tmpDir, { recursive: true }); } } - + return sentinels } } From d9a6bb376f9e6a54959c4424aad2e4c636071164 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 8 Jul 2025 14:28:50 +0300 Subject: [PATCH 178/244] chore(release): use deploy keys for relese (#3013) main branch is protected and does not allow direct pushes. the release action needs to push. branch protection rules can be bypassed for people and apps, but not github actions. one of the workarounds is to use a ruleset in which we set a deploy key see: https://github.com/orgs/community/discussions/25305\#discussioncomment-10728028 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5732f2eda0..e7c9d58fe71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ssh-key: ${{ secrets.RELEASE_KEY }} - name: Setup Node.js uses: actions/setup-node@v4 From 748cad2e7f71df66e8787d78720f02a1b394ad82 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:25 +0000 Subject: [PATCH 179/244] Release client@5.6.0 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9fc9f93f92..f17556e3a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", + "integrity": "sha512-M3Svdwt6oSfyfQdqEr0L2HOJH2vK7GgCFx1NfAQvpWAT4+ljoT1L5S5cKT3dA9NJrxrOPDkdoTPWJnIrGCOcmw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/client/package.json b/packages/client/package.json index b95d1087d07..ee98d77ca1f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From b7a5f40ab154dd293f630af90f1370b2e6a836e2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:32 +0000 Subject: [PATCH 180/244] Release bloom@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f17556e3a04..fb9d1d164ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.5.6.tgz", + "integrity": "sha512-bNR3mxkwtfuCxNOzfV8B3R5zA1LiN57EH6zK4jVBIgzMzliNuReZXBFGnXvsi80/SYohajn78YdpYI+XNpqL+A==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 83dd6f893f9..47d1f6978ef 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*" From dab595d8d9a70d3efe957e1ac06b6f6639ecb8d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:38 +0000 Subject: [PATCH 181/244] Release json@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb9d1d164ad..cd844a0de51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.5.6.tgz", + "integrity": "sha512-AIsoe3SsGQagqAmSQHaqxEinm5oCWr7zxPWL90kKaEdLJ+zw8KBznf2i9oK0WUFP5pFssSQUXqnscQKe2amfDQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/json/package.json b/packages/json/package.json index 0d77a3bc6b1..edfdaec8efa 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*" From ff5c6b3066a2e821a62fe617ec1032be456a90ec Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:44 +0000 Subject: [PATCH 182/244] Release search@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd844a0de51..2b21ffe23a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.5.6" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.5.6.tgz", + "integrity": "sha512-JSqasYqO0mVcHL7oxvbySRBBZYRYhFl3W7f0Da7BW8M/r0Z9wCiVrdjnN4/mKBpWZkoJT/iuisLUdPGhpKxBew==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 30edac8003e..c876d7460c3 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*" From a6193a770c141974d499bd35934d0cf880858b8f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:51 +0000 Subject: [PATCH 183/244] Release time-series@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b21ffe23a9..4686dcf150e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.5.6" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.5.6.tgz", + "integrity": "sha512-jkpcgq3NOI3TX7xEAJ3JgesJTxAx7k0m6lNxNsYdEM8KOl+xj7GaB/0CbLkoricZDmFSEAz7ClA1iK9XkGHf+Q==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.0", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index f90bc8f5f9b..762aea77661 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*" From e5b2466da3fb3f1ff17ca58f7e96148e7045992b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:56 +0000 Subject: [PATCH 184/244] Release entraid@5.6.0 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4686dcf150e..106d5dfb591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 79641cdf9a9..173bd0a6c67 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@types/express": "^4.17.21", From 4d886b88661a21be7b1ea4b44bd58c9ca19dffc1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:38:02 +0000 Subject: [PATCH 185/244] Release redis@5.6.0 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 106d5dfb591..b8ff84d8259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.5.6", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.5.6", - "@redis/client": "5.5.6", - "@redis/json": "5.5.6", - "@redis/search": "5.5.6", - "@redis/time-series": "5.5.6" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.5.6.tgz", - "integrity": "sha512-bNR3mxkwtfuCxNOzfV8B3R5zA1LiN57EH6zK4jVBIgzMzliNuReZXBFGnXvsi80/SYohajn78YdpYI+XNpqL+A==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.5.6" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", - "integrity": "sha512-M3Svdwt6oSfyfQdqEr0L2HOJH2vK7GgCFx1NfAQvpWAT4+ljoT1L5S5cKT3dA9NJrxrOPDkdoTPWJnIrGCOcmw==", + "version": "5.6.0", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.6.0", + "@redis/client": "5.6.0", + "@redis/json": "5.6.0", + "@redis/search": "5.6.0", + "@redis/time-series": "5.6.0" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.5.6.tgz", - "integrity": "sha512-AIsoe3SsGQagqAmSQHaqxEinm5oCWr7zxPWL90kKaEdLJ+zw8KBznf2i9oK0WUFP5pFssSQUXqnscQKe2amfDQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.5.6" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.5.6.tgz", - "integrity": "sha512-JSqasYqO0mVcHL7oxvbySRBBZYRYhFl3W7f0Da7BW8M/r0Z9wCiVrdjnN4/mKBpWZkoJT/iuisLUdPGhpKxBew==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.5.6" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.5.6.tgz", - "integrity": "sha512-jkpcgq3NOI3TX7xEAJ3JgesJTxAx7k0m6lNxNsYdEM8KOl+xj7GaB/0CbLkoricZDmFSEAz7ClA1iK9XkGHf+Q==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.5.6" - } - }, "packages/search": { "name": "@redis/search", "version": "5.6.0", diff --git a/packages/redis/package.json b/packages/redis/package.json index bf5ea798e70..32251c7e501 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.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.5.6", - "@redis/client": "5.5.6", - "@redis/json": "5.5.6", - "@redis/search": "5.5.6", - "@redis/time-series": "5.5.6" + "@redis/bloom": "5.6.0", + "@redis/client": "5.6.0", + "@redis/json": "5.6.0", + "@redis/search": "5.6.0", + "@redis/time-series": "5.6.0" }, "engines": { "node": ">= 18" From 987515c69b51bf4ed4eca718a38c7ed2ef49c9b4 Mon Sep 17 00:00:00 2001 From: Clo Junseo Kim Date: Thu, 10 Jul 2025 17:22:51 +0900 Subject: [PATCH 186/244] docs: fix hyperlink for lua scripts, functions (#3015) --- docs/clustering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/clustering.md b/docs/clustering.md index f335c259c24..6803583fa51 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -35,8 +35,8 @@ await cluster.close(); | maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors | | nodeAddressMap | | Defines the [node address mapping](#node-address-map) | | 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)) | +| scripts | | Script definitions (see [Lua Scripts](./programmability.md#lua-scripts)) | +| functions | | Function definitions (see [Functions](./programmability.md#functions)) | ## Auth with password and username From c21dd924fe8aa03c7abe1a7733a564fd3fc74011 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 17 Jul 2025 14:02:22 +0300 Subject: [PATCH 187/244] fix(search): adjust field types for ft.search (#3018) In ft.search SORTBY, SUMMARIZE and HIGHLIGHT all take string arguments and do not need @ or $ as a prefix. In fact, if you put @ or $ there, redis returns error like this: [SimpleError: Property `@age` not loaded nor in schema] fixes #3017 --- packages/search/lib/commands/SEARCH.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index b8efda05777..61e1d8d84d2 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -1,7 +1,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 { RediSearchLanguage } from './CREATE'; import { DEFAULT_DIALECT } from '../dialect/default'; export type FtSearchParams = Record; @@ -32,13 +32,13 @@ export interface FtSearchOptions { INFIELDS?: RedisVariadicArgument; RETURN?: RedisVariadicArgument; SUMMARIZE?: boolean | { - FIELDS?: RediSearchProperty | Array; + FIELDS?: RedisArgument | Array; FRAGS?: number; LEN?: number; SEPARATOR?: RedisArgument; }; HIGHLIGHT?: boolean | { - FIELDS?: RediSearchProperty | Array; + FIELDS?: RedisArgument | Array; TAGS?: { open: RedisArgument; close: RedisArgument; @@ -51,7 +51,7 @@ export interface FtSearchOptions { EXPANDER?: RedisArgument; SCORER?: RedisArgument; SORTBY?: RedisArgument | { - BY: RediSearchProperty; + BY: RedisArgument; DIRECTION?: 'ASC' | 'DESC'; }; LIMIT?: { @@ -133,7 +133,7 @@ export function parseSearchOptions(parser: CommandParser, options?: FtSearchOpti if (options?.SORTBY) { parser.push('SORTBY'); - + if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) { parser.push(options.SORTBY); } else { @@ -193,7 +193,7 @@ export default { value: withoutDocuments ? Object.create(null) : documentValue(reply[i++]) }); } - + return { total: reply[0], documents From 539fe52236d54b75a9413e8b97b70908fa98b4f4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 21 Jul 2025 18:17:07 +0300 Subject: [PATCH 188/244] fix(client): make socket.host not required (#3024) Underlying node tls.ConnectionOptions does not require host, so we shouldnt as well. Further, if `url` is provided in the upper level config, it takes precedence, which could be misleading: createClient({ url: 'rediss://user:secret@localhost:6379/0', socket: { tls: true, host: 'somehost' <-- this gets overwritten to `localhost` } }); fixes #3023 --- packages/client/lib/client/socket.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts index 58ccbe0b0c5..5f0bcc44929 100644 --- a/packages/client/lib/client/socket.ts +++ b/packages/client/lib/client/socket.ts @@ -38,7 +38,6 @@ type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit< type RedisTlsOptions = RedisSocketOptionsCommon & tls.ConnectionOptions & { tls: true; - host: string; } type RedisIpcOptions = RedisSocketOptionsCommon & Omit< @@ -238,7 +237,7 @@ export default class RedisSocket extends EventEmitter { } } while (this.#isOpen && !this.#isReady); } - + async #createSocket(): Promise { const socket = this.#socketFactory.create(); @@ -293,7 +292,7 @@ export default class RedisSocket extends EventEmitter { write(iterable: Iterable>) { if (!this.#socket) return; - + this.#socket.cork(); for (const args of iterable) { for (const toWrite of args) { @@ -364,7 +363,7 @@ export default class RedisSocket extends EventEmitter { 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; } } From ddd2cc5185dc3a0cdb36c84046f5a1ab7dc682eb Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 23 Jul 2025 13:41:30 +0300 Subject: [PATCH 189/244] fix(client): export RedisJSON type (#3026) fixes #3014 --- packages/json/lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/json/lib/index.ts b/packages/json/lib/index.ts index bc0e103e8c8..1993f9ef42d 100644 --- a/packages/json/lib/index.ts +++ b/packages/json/lib/index.ts @@ -1 +1,2 @@ export { default } from './commands'; +export type { RedisJSON } from './commands'; From 0541b32f34045faaff8538072d63cafbede5a4fc Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:16:08 +0300 Subject: [PATCH 190/244] docs: Update doctests (#3020) * move all doctests from emb-examples branch * fix readme * add package-lock.json * --wip-- [skip ci] * fix: replace client.quit() with client.close() as quit is deprecated - doctests/cmds-hash.js - doctests/cmds-list.js - doctests/cmds-servermgmt.js - doctests/cmds-set.js * fix: replace client.quit() with client.close() as quit is deprecated - doctests/cmds-sorted-set.js - doctests/cmds-string.js - doctests/dt-bitfield.js - doctests/dt-bitmap.js * fix: replace client.quit() with client.close() as quit is deprecated - dt-bloom.js: replace client.quit() with client.close() - dt-cms.js: replace client.quit() with client.close() - dt-cuckoo.js: replace client.quit() with client.close() and update expected output comments to reflect v5 boolean returns - dt-geo.js: replace client.quit() with client.close() * fix(doctests): correct pfAdd return values and replace quit with close - Fix dt-hll.js: pfAdd returns 1 instead of true in comments and assertions - Fix dt-hash.js and dt-hll.js: replace deprecated client.quit() with client.close() * fix(doctests): correct API usage and return values in json and list examples - Fix dt-json.js: use options object for json.type, json.strLen, json.del, json.arrPop, json.objLen, json.objKeys - Fix dt-json.js: correct json.del return value from [1] to 1 - Fix dt-list.js: correct client initialization, return values (null, OK, 1), and error type - Replace deprecated client.quit() with client.close() in both files * fix(doctests): update dt-set.js and dt-ss.js for v5 compliance - Updated boolean return values to numbers for SISMEMBER and SMISMEMBER commands - Fixed client lifecycle to use client.close() instead of client.quit() - Removed unnecessary await from createClient() - Added order-independent assertions for set operations - Removed debug statement * fix(doctests): update deprecated methods and imports for v5 compliance - Fix dt-string.js: remove await from client creation and replace client.quit() with client.close() - Fix dt-tdigest.js: replace deprecated client.quit() with client.close() - Fix dt-topk.js: replace client.quit() with client.close() and fix output comment from [1, 0] to [true, false] - Fix query-agg.js: update @redis/search imports to use new constant names and replace client.disconnect() with client.close() * fix(doctests): update imports and replace deprecated disconnect with close - Replace SchemaFieldTypes/VectorAlgorithms with SCHEMA_FIELD_TYPE/SCHEMA_VECTOR_FIELD_ALGORITHM - Replace client.disconnect() with client.close() for consistent deprecation handling - Update query-combined.js, query-em.js, query-ft.js, and query-geo.js * fix(doctests): update imports and replace deprecated methods in remaining files - Update imports to use SCHEMA_FIELD_TYPE and SCHEMA_VECTOR_FIELD_ALGORITHM constants - Replace deprecated disconnect() and quit() methods with close() - Fix assertion in search-quickstart.js to use correct bicycle ID * fix(doctests): update cmds-generic.js and cmds-cnxmgmt.js for v5 compliance - Replace deprecated client.quit() with client.close() - Update sScanIterator to use collection-yielding behavior (value -> values) - Fix HSCAN API changes: tuples renamed to entries - Fix cursor type issues: use string '0' instead of number 0 for hScan - Fix infinite loop in scan cleanup by using do-while pattern * fix(doctests): update dt-streams.js object shapes and parameters for v5 compliance - Update stream result objects from tuple format to proper object format with id/message properties - Change xRead/xReadGroup results from nested arrays to objects with name/messages structure - Update xAutoClaim results to use nextId, messages, and deletedMessages properties - Add missing properties to xInfo* results (max-deleted-entry-id, entries-added, recorded-first-entry-id, entries-read, lag, inactive) - Modernize parameter names (count -> COUNT, block -> BLOCK, etc.) - Update MAXLEN/APPROXIMATE options to new TRIM object structure - Fix error message format for XADD duplicate ID error - Update boolean return values (True -> OK) --------- Co-authored-by: Nikolay Karadzhov --- doctests/README.md | 32 + doctests/cmds-cnxmgmt.js | 49 + doctests/cmds-generic.js | 195 ++ doctests/cmds-hash.js | 109 + doctests/cmds-list.js | 129 + doctests/cmds-servermgmt.js | 45 + doctests/cmds-set.js | 44 + doctests/cmds-sorted-set.js | 115 + doctests/cmds-string.js | 27 + doctests/data/query_em.json | 92 + doctests/data/query_vector.json | 3952 ++++++++++++++++++++++++++ doctests/dt-bitfield.js | 76 + doctests/dt-bitmap.js | 39 + doctests/dt-bloom.js | 46 + doctests/dt-cms.js | 50 + doctests/dt-cuckoo.js | 38 + doctests/dt-geo.js | 59 + doctests/dt-hash.js | 98 + doctests/dt-hll.js | 38 + doctests/dt-json.js | 425 +++ doctests/dt-list.js | 329 +++ doctests/dt-set.js | 176 ++ doctests/dt-ss.js | 162 ++ doctests/dt-streams.js | 366 +++ doctests/dt-string.js | 68 + doctests/dt-tdigest.js | 85 + doctests/dt-topk.js | 48 + doctests/package-lock.json | 889 ++++++ doctests/package.json | 12 + doctests/query-agg.js | 139 + doctests/query-combined.js | 191 ++ doctests/query-em.js | 121 + doctests/query-ft.js | 84 + doctests/query-geo.js | 82 + doctests/query-range.js | 98 + doctests/query-vector.js | 110 + doctests/run_examples.sh | 15 + doctests/search-quickstart.js | 231 ++ doctests/string-set-get-example.js | 27 + packages/client/lib/commands/SCAN.ts | 14 +- 40 files changed, 8899 insertions(+), 6 deletions(-) create mode 100644 doctests/README.md create mode 100644 doctests/cmds-cnxmgmt.js create mode 100644 doctests/cmds-generic.js create mode 100644 doctests/cmds-hash.js create mode 100644 doctests/cmds-list.js create mode 100644 doctests/cmds-servermgmt.js create mode 100644 doctests/cmds-set.js create mode 100644 doctests/cmds-sorted-set.js create mode 100644 doctests/cmds-string.js create mode 100644 doctests/data/query_em.json create mode 100644 doctests/data/query_vector.json create mode 100644 doctests/dt-bitfield.js create mode 100644 doctests/dt-bitmap.js create mode 100644 doctests/dt-bloom.js create mode 100644 doctests/dt-cms.js create mode 100644 doctests/dt-cuckoo.js create mode 100644 doctests/dt-geo.js create mode 100644 doctests/dt-hash.js create mode 100644 doctests/dt-hll.js create mode 100644 doctests/dt-json.js create mode 100644 doctests/dt-list.js create mode 100644 doctests/dt-set.js create mode 100644 doctests/dt-ss.js create mode 100644 doctests/dt-streams.js create mode 100644 doctests/dt-string.js create mode 100644 doctests/dt-tdigest.js create mode 100644 doctests/dt-topk.js create mode 100644 doctests/package-lock.json create mode 100644 doctests/package.json create mode 100644 doctests/query-agg.js create mode 100644 doctests/query-combined.js create mode 100644 doctests/query-em.js create mode 100644 doctests/query-ft.js create mode 100644 doctests/query-geo.js create mode 100644 doctests/query-range.js create mode 100644 doctests/query-vector.js create mode 100755 doctests/run_examples.sh create mode 100644 doctests/search-quickstart.js create mode 100644 doctests/string-set-get-example.js diff --git a/doctests/README.md b/doctests/README.md new file mode 100644 index 00000000000..59d1cb0364c --- /dev/null +++ b/doctests/README.md @@ -0,0 +1,32 @@ +# Command examples for redis.io + +## Setup + +To set up the examples folder so that you can run an example / develop one of your own: + +``` +$ git clone https://github.com/redis/node-redis.git +$ cd node-redis +$ npm install -ws && npm run build +$ cd doctests +$ npm install +``` + +## How to add examples + +Create regular node file in the current folder with meaningful name. It makes sense prefix example files with +command category (e.g. string, set, list, hash, etc) to make navigation in the folder easier. + +### Special markup + +See https://github.com/redis-stack/redis-stack-website#readme for more details. + +## How to test the examples + +Just include necessary assertions in the example file and run +```bash +sh doctests/run_examples.sh +``` +to test all examples in the current folder. + +See `tests.js` for more details. diff --git a/doctests/cmds-cnxmgmt.js b/doctests/cmds-cnxmgmt.js new file mode 100644 index 00000000000..8b616b6f105 --- /dev/null +++ b/doctests/cmds-cnxmgmt.js @@ -0,0 +1,49 @@ +// EXAMPLE: cmds_cnxmgmt +// REMOVE_START +import assert from "node:assert"; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START auth1 +// REMOVE_START +await client.sendCommand(['CONFIG', 'SET', 'requirepass', 'temp_pass']); +// REMOVE_END +const res1 = await client.auth({ password: 'temp_pass' }); +console.log(res1); // OK + +const res2 = await client.auth({ username: 'default', password: 'temp_pass' }); +console.log(res2); // OK + +// REMOVE_START +assert.equal(res1, "OK"); +assert.equal(res2, "OK"); +await client.sendCommand(['CONFIG', 'SET', 'requirepass', '']); +// REMOVE_END +// STEP_END + +// STEP_START auth2 +// REMOVE_START +await client.sendCommand([ + 'ACL', 'SETUSER', 'test-user', + 'on', '>strong_password', '+acl' +]); +// REMOVE_END +const res3 = await client.auth({ username: 'test-user', password: 'strong_password' }); +console.log(res3); // OK + +// REMOVE_START +assert.equal(res3, "OK"); +await client.auth({ username: 'default', password: '' }) +await client.sendCommand(['ACL', 'DELUSER', 'test-user']); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-generic.js b/doctests/cmds-generic.js new file mode 100644 index 00000000000..50329ede460 --- /dev/null +++ b/doctests/cmds-generic.js @@ -0,0 +1,195 @@ +// EXAMPLE: cmds_generic +// REMOVE_START +import assert from "node:assert"; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START del +const delRes1 = await client.set('key1', 'Hello'); +console.log(delRes1); // OK + +const delRes2 = await client.set('key2', 'World'); +console.log(delRes2); // OK + +const delRes3 = await client.del(['key1', 'key2', 'key3']); +console.log(delRes3); // 2 +// REMOVE_START +assert.equal(delRes3, 2); +// REMOVE_END +// STEP_END + +// STEP_START expire +const expireRes1 = await client.set('mykey', 'Hello'); +console.log(expireRes1); // OK + +const expireRes2 = await client.expire('mykey', 10); +console.log(expireRes2); // 1 + +const expireRes3 = await client.ttl('mykey'); +console.log(expireRes3); // 10 +// REMOVE_START +assert.equal(expireRes3, 10); +// REMOVE_END + +const expireRes4 = await client.set('mykey', 'Hello World'); +console.log(expireRes4); // OK + +const expireRes5 = await client.ttl('mykey'); +console.log(expireRes5); // -1 +// REMOVE_START +assert.equal(expireRes5, -1); +// REMOVE_END + +const expireRes6 = await client.expire('mykey', 10, "XX"); +console.log(expireRes6); // 0 +// REMOVE_START +assert.equal(expireRes6, 0) +// REMOVE_END + +const expireRes7 = await client.ttl('mykey'); +console.log(expireRes7); // -1 +// REMOVE_START +assert.equal(expireRes7, -1); +// REMOVE_END + +const expireRes8 = await client.expire('mykey', 10, "NX"); +console.log(expireRes8); // 1 +// REMOVE_START +assert.equal(expireRes8, 1); +// REMOVE_END + +const expireRes9 = await client.ttl('mykey'); +console.log(expireRes9); // 10 +// REMOVE_START +assert.equal(expireRes9, 10); +await client.del('mykey'); +// REMOVE_END +// STEP_END + +// STEP_START ttl +const ttlRes1 = await client.set('mykey', 'Hello'); +console.log(ttlRes1); // OK + +const ttlRes2 = await client.expire('mykey', 10); +console.log(ttlRes2); // 1 + +const ttlRes3 = await client.ttl('mykey'); +console.log(ttlRes3); // 10 +// REMOVE_START +assert.equal(ttlRes3, 10); +await client.del('mykey'); +// REMOVE_END +// STEP_END + +// STEP_START scan1 +const scan1Res1 = await client.sAdd('myset', ['1', '2', '3', 'foo', 'foobar', 'feelsgood']); +console.log(scan1Res1); // 6 + +let scan1Res2 = []; +for await (const values of client.sScanIterator('myset', { MATCH: 'f*' })) { + scan1Res2 = scan1Res2.concat(values); +} +console.log(scan1Res2); // ['foo', 'foobar', 'feelsgood'] +// REMOVE_START +console.assert(scan1Res2.sort().toString() === ['foo', 'foobar', 'feelsgood'].sort().toString()); +await client.del('myset'); +// REMOVE_END +// STEP_END + +// STEP_START scan2 +// REMOVE_START +for (let i = 1; i <= 1000; i++) { + await client.set(`key:${i}`, i); +} +// REMOVE_END +let cursor = '0'; +let scanResult; + +scanResult = await client.scan(cursor, { MATCH: '*11*' }); +console.log(scanResult.cursor, scanResult.keys); + +scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' }); +console.log(scanResult.cursor, scanResult.keys); + +scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' }); +console.log(scanResult.cursor, scanResult.keys); + +scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*' }); +console.log(scanResult.cursor, scanResult.keys); + +scanResult = await client.scan(scanResult.cursor, { MATCH: '*11*', COUNT: 1000 }); +console.log(scanResult.cursor, scanResult.keys); +// REMOVE_START +console.assert(scanResult.keys.length === 18); +cursor = '0'; +const prefix = 'key:*'; +do { + scanResult = await client.scan(cursor, { MATCH: prefix, COUNT: 1000 }); + console.log(scanResult.cursor, scanResult.keys); + cursor = scanResult.cursor; + const keys = scanResult.keys; + if (keys.length) { + await client.del(keys); + } +} while (cursor !== '0'); +// REMOVE_END +// STEP_END + +// STEP_START scan3 +const scan3Res1 = await client.geoAdd('geokey', { longitude: 0, latitude: 0, member: 'value' }); +console.log(scan3Res1); // 1 + +const scan3Res2 = await client.zAdd('zkey', [{ score: 1000, value: 'value' }]); +console.log(scan3Res2); // 1 + +const scan3Res3 = await client.type('geokey'); +console.log(scan3Res3); // zset +// REMOVE_START +console.assert(scan3Res3 === 'zset'); +// REMOVE_END + +const scan3Res4 = await client.type('zkey'); +console.log(scan3Res4); // zset +// REMOVE_START +console.assert(scan3Res4 === 'zset'); +// REMOVE_END + +const scan3Res5 = await client.scan('0', { TYPE: 'zset' }); +console.log(scan3Res5.keys); // ['zkey', 'geokey'] +// REMOVE_START +console.assert(scan3Res5.keys.sort().toString() === ['zkey', 'geokey'].sort().toString()); +await client.del(['geokey', 'zkey']); +// REMOVE_END +// STEP_END + +// STEP_START scan4 +const scan4Res1 = await client.hSet('myhash', { a: 1, b: 2 }); +console.log(scan4Res1); // 2 + +const scan4Res2 = await client.hScan('myhash', '0'); +console.log(scan4Res2.entries); // [{field: 'a', value: '1'}, {field: 'b', value: '2'}] +// REMOVE_START +assert.deepEqual(scan4Res2.entries, [ + { field: 'a', value: '1' }, + { field: 'b', value: '2' } +]); +// REMOVE_END + +const scan4Res3 = await client.hScan('myhash', '0', { COUNT: 10 }); +const items = scan4Res3.entries.map((item) => item.field) +console.log(items); // ['a', 'b'] +// REMOVE_START +assert.deepEqual(items, ['a', 'b']) +await client.del('myhash'); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-hash.js b/doctests/cmds-hash.js new file mode 100644 index 00000000000..8ce29785763 --- /dev/null +++ b/doctests/cmds-hash.js @@ -0,0 +1,109 @@ +// EXAMPLE: cmds_hash +// HIDE_START +import assert from 'node:assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START hset +const res1 = await client.hSet('myhash', 'field1', 'Hello') +console.log(res1) // 1 + +const res2 = await client.hGet('myhash', 'field1') +console.log(res2) // Hello + +const res3 = await client.hSet( + 'myhash', + { + 'field2': 'Hi', + 'field3': 'World' + } +) +console.log(res3) // 2 + +const res4 = await client.hGet('myhash', 'field2') +console.log(res4) // Hi + +const res5 = await client.hGet('myhash', 'field3') +console.log(res5) // World + +const res6 = await client.hGetAll('myhash') +console.log(res6) + +// REMOVE_START +assert.equal(res1, 1); +assert.equal(res2, 'Hello'); +assert.equal(res3, 2); +assert.equal(res4, 'Hi'); +assert.equal(res5, 'World'); +assert.deepEqual(res6, { + field1: 'Hello', + field2: 'Hi', + field3: 'World' +}); +await client.del('myhash') +// REMOVE_END +// STEP_END + +// STEP_START hget +const res7 = await client.hSet('myhash', 'field1', 'foo') +console.log(res7) // 1 + +const res8 = await client.hGet('myhash', 'field1') +console.log(res8) // foo + +const res9 = await client.hGet('myhash', 'field2') +console.log(res9) // null + +// REMOVE_START +assert.equal(res7, 1); +assert.equal(res8, 'foo'); +assert.equal(res9, null); +await client.del('myhash') +// REMOVE_END +// STEP_END + +// STEP_START hgetall +const res10 = await client.hSet( + 'myhash', + { + 'field1': 'Hello', + 'field2': 'World' + } +) + +const res11 = await client.hGetAll('myhash') +console.log(res11) // [Object: null prototype] { field1: 'Hello', field2: 'World' } + +// REMOVE_START +assert.deepEqual(res11, { + field1: 'Hello', + field2: 'World' +}); +await client.del('myhash') +// REMOVE_END +// STEP_END + +// STEP_START hvals +const res12 = await client.hSet( + 'myhash', + { + 'field1': 'Hello', + 'field2': 'World' + } +) + +const res13 = await client.hVals('myhash') +console.log(res13) // [ 'Hello', 'World' ] + +// REMOVE_START +assert.deepEqual(res13, [ 'Hello', 'World' ]); +await client.del('myhash') +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-list.js b/doctests/cmds-list.js new file mode 100644 index 00000000000..9d1b4154dfe --- /dev/null +++ b/doctests/cmds-list.js @@ -0,0 +1,129 @@ +// EXAMPLE: cmds_list +// HIDE_START +import assert from 'node:assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START lpush +const res1 = await client.lPush('mylist', 'world'); +console.log(res1); // 1 + +const res2 = await client.lPush('mylist', 'hello'); +console.log(res2); // 2 + +const res3 = await client.lRange('mylist', 0, -1); +console.log(res3); // [ 'hello', 'world' ] + +// REMOVE_START +assert.deepEqual(res3, [ 'hello', 'world' ]); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// STEP_START lrange +const res4 = await client.rPush('mylist', 'one'); +console.log(res4); // 1 + +const res5 = await client.rPush('mylist', 'two'); +console.log(res5); // 2 + +const res6 = await client.rPush('mylist', 'three'); +console.log(res6); // 3 + +const res7 = await client.lRange('mylist', 0, 0); +console.log(res7); // [ 'one' ] + +const res8 = await client.lRange('mylist', -3, 2); +console.log(res8); // [ 'one', 'two', 'three' ] + +const res9 = await client.lRange('mylist', -100, 100); +console.log(res9); // [ 'one', 'two', 'three' ] + +const res10 = await client.lRange('mylist', 5, 10); +console.log(res10); // [] + +// REMOVE_START +assert.deepEqual(res7, [ 'one' ]); +assert.deepEqual(res8, [ 'one', 'two', 'three' ]); +assert.deepEqual(res9, [ 'one', 'two', 'three' ]); +assert.deepEqual(res10, []); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// STEP_START llen +const res11 = await client.lPush('mylist', 'World'); +console.log(res11); // 1 + +const res12 = await client.lPush('mylist', 'Hello'); +console.log(res12); // 2 + +const res13 = await client.lLen('mylist'); +console.log(res13); // 2 + +// REMOVE_START +assert.equal(res13, 2); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// STEP_START rpush +const res14 = await client.rPush('mylist', 'hello'); +console.log(res14); // 1 + +const res15 = await client.rPush('mylist', 'world'); +console.log(res15); // 2 + +const res16 = await client.lRange('mylist', 0, -1); +console.log(res16); // [ 'hello', 'world' ] + +// REMOVE_START +assert.deepEqual(res16, [ 'hello', 'world' ]); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// STEP_START lpop +const res17 = await client.rPush('mylist', ["one", "two", "three", "four", "five"]); +console.log(res17); // 5 + +const res18 = await client.lPop('mylist'); +console.log(res18); // 'one' + +const res19 = await client.lPopCount('mylist', 2); +console.log(res19); // [ 'two', 'three' ] + +const res20 = await client.lRange('mylist', 0, -1); +console.log(res20); // [ 'four', 'five' ] + +// REMOVE_START +assert.deepEqual(res20, [ 'four', 'five' ]); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// STEP_START rpop +const res21 = await client.rPush('mylist', ["one", "two", "three", "four", "five"]); +console.log(res21); // 5 + +const res22 = await client.rPop('mylist'); +console.log(res22); // 'five' + +const res23 = await client.rPopCount('mylist', 2); +console.log(res23); // [ 'four', 'three' ] + +const res24 = await client.lRange('mylist', 0, -1); +console.log(res24); // [ 'one', 'two' ] + +// REMOVE_START +assert.deepEqual(res24, [ 'one', 'two' ]); +await client.del('mylist'); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-servermgmt.js b/doctests/cmds-servermgmt.js new file mode 100644 index 00000000000..0a53e05919d --- /dev/null +++ b/doctests/cmds-servermgmt.js @@ -0,0 +1,45 @@ +// EXAMPLE: cmds_servermgmt +// REMOVE_START +import assert from 'node:assert'; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START flushall +// REMOVE_START +await client.set('foo', '1'); +await client.set('bar', '2'); +await client.set('baz', '3'); +// REMOVE_END +const res1 = await client.flushAll('SYNC'); // or ASYNC +console.log(res1); // OK + +const res2 = await client.keys('*'); +console.log(res2); // [] + +// REMOVE_START +assert.equal(res1, 'OK'); +assert.deepEqual(res2, []); +// REMOVE_END +// STEP_END + +// STEP_START info +const res3 = await client.info(); +console.log(res3) +// # Server +// redis_version:7.4.0 +// redis_git_sha1:c9d29f6a +// redis_git_dirty:0 +// redis_build_id:4c367a16e3f9616 +// redis_mode:standalone +// ... +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-set.js b/doctests/cmds-set.js new file mode 100644 index 00000000000..8a9a3837036 --- /dev/null +++ b/doctests/cmds-set.js @@ -0,0 +1,44 @@ +// EXAMPLE: cmds_set +// REMOVE_START +import assert from 'node:assert'; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START sadd +const res1 = await client.sAdd('myset', ['Hello', 'World']); +console.log(res1); // 2 + +const res2 = await client.sAdd('myset', ['World']); +console.log(res2); // 0 + +const res3 = await client.sMembers('myset') +console.log(res3); // ['Hello', 'World'] + +// REMOVE_START +assert.deepEqual(res3, ['Hello', 'World']); +await client.del('myset'); +// REMOVE_END +// STEP_END + +// STEP_START smembers +const res4 = await client.sAdd('myset', ['Hello', 'World']); +console.log(res4); // 2 + +const res5 = await client.sMembers('myset') +console.log(res5); // ['Hello', 'World'] + +// REMOVE_START +assert.deepEqual(res5, ['Hello', 'World']); +await client.del('myset'); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-sorted-set.js b/doctests/cmds-sorted-set.js new file mode 100644 index 00000000000..b718938cc2b --- /dev/null +++ b/doctests/cmds-sorted-set.js @@ -0,0 +1,115 @@ +// EXAMPLE: cmds_sorted_set +// REMOVE_START +import assert from "node:assert"; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +client.on('error', err => console.log('Redis Client Error', err)); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START zadd +const val1 = await client.zAdd("myzset", [{ value: 'one', score: 1 }]); +console.log(val1); +// returns 1 + +const val2 = await client.zAdd("myzset", [{ value: 'uno', score: 1 }]); +console.log(val2); +// returns 1 + +const val3 = await client.zAdd("myzset", [{ value: 'two', score: 2 }, { value: 'three', score: 3 }]); +console.log(val3); +// returns 2 + +const val4 = await client.zRangeWithScores("myzset", 0, -1); +console.log(val4); +// returns [{value: 'one', score: 1}, {value: 'uno', score: 1}, {value: 'two', score: 2}, {value: 'three', score: 3} ] + +// REMOVE_START +assert.equal(val1, 1); +assert.equal(val2, 1); +assert.equal(val3, 2); +assert.deepEqual(val4, [ + { value: 'one', score: 1 }, + { value: 'uno', score: 1 }, + { value: 'two', score: 2 }, + { value: 'three', score: 3 } +]); +await client.del('myzset'); +// REMOVE_END +// STEP_END + +// STEP_START zrange1 +const val5 = await client.zAdd("myzset", [ + { value: 'one', score: 1 }, + { value: 'two', score: 2 }, + { value: 'three', score: 3 } +]); +console.log(val5); +// returns 3 + +const val6 = await client.zRange('myzset', 0, -1); +console.log(val6); +// returns ['one', 'two', 'three'] +// REMOVE_START +console.assert(JSON.stringify(val6) === JSON.stringify(['one', 'two', 'three'])); +// REMOVE_END + +const val7 = await client.zRange('myzset', 2, 3); +console.log(val7); +// returns ['three'] +// REMOVE_START +console.assert(JSON.stringify(val7) === JSON.stringify(['three'])); +// REMOVE_END + +const val8 = await client.zRange('myzset', -2, -1); +console.log(val8); +// returns ['two', 'three'] +// REMOVE_START +console.assert(JSON.stringify(val8) === JSON.stringify(['two', 'three'])); +await client.del('myzset'); +// REMOVE_END +// STEP_END + +// STEP_START zrange2 +const val9 = await client.zAdd("myzset", [ + { value: 'one', score: 1 }, + { value: 'two', score: 2 }, + { value: 'three', score: 3 } +]); +console.log(val9); +// returns 3 + +const val10 = await client.zRangeWithScores('myzset', 0, 1); +console.log(val10); +// returns [{value: 'one', score: 1}, {value: 'two', score: 2}] +// REMOVE_START +console.assert(JSON.stringify(val10) === JSON.stringify([{value: 'one', score: 1}, {value: 'two', score: 2}])); +await client.del('myzset'); +// REMOVE_END +// STEP_END + +// STEP_START zrange3 +const val11 = await client.zAdd("myzset", [ + { value: 'one', score: 1 }, + { value: 'two', score: 2 }, + { value: 'three', score: 3 } +]); +console.log(val11); +// returns 3 + +const val12 = await client.zRange('myzset', 2, 3, { BY: 'SCORE', LIMIT: { offset: 1, count: 1 } }); +console.log(val12); +// >>> ['three'] +// REMOVE_START +console.assert(JSON.stringify(val12) === JSON.stringify(['three'])); +await client.del('myzset'); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/cmds-string.js b/doctests/cmds-string.js new file mode 100644 index 00000000000..00b3f738fa3 --- /dev/null +++ b/doctests/cmds-string.js @@ -0,0 +1,27 @@ +// EXAMPLE: cmds_string +// REMOVE_START +import assert from "node:assert"; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); +client.on('error', err => console.log('Redis Client Error', err)); +await client.connect().catch(console.error); +// HIDE_END + +// STEP_START incr +await client.set("mykey", "10"); +const value1 = await client.incr("mykey"); +console.log(value1); +// returns 11 +// REMOVE_START +assert.equal(value1, 11); +await client.del('mykey'); +// REMOVE_END +// STEP_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/doctests/data/query_em.json b/doctests/data/query_em.json new file mode 100644 index 00000000000..bcf908aaf85 --- /dev/null +++ b/doctests/data/query_em.json @@ -0,0 +1,92 @@ +[ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new" + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, -118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!", + "condition": "used" + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, -87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.", + "condition": "used" + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!", + "condition": "used" + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, -122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.", + "condition": "used" + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s not to say that it’s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.", + "condition": "new" + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It’s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.", + "condition": "new" + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new" + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you’re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.", + "condition": "new" + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, 12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.", + "condition": "refurbished" + } +] diff --git a/doctests/data/query_vector.json b/doctests/data/query_vector.json new file mode 100644 index 00000000000..625479e111b --- /dev/null +++ b/doctests/data/query_vector.json @@ -0,0 +1,3952 @@ +[ + { + "brand": "Velorim", + "condition": "new", + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids\u2019 pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.", + "description_embeddings": [ + -0.026918452233076096, + 0.07200391590595245, + 0.019199736416339874, + -0.024749649688601494, + -0.09264523535966873, + 0.017702950164675713, + 0.11252444237470627, + 0.09377790987491608, + 0.005099582951515913, + 0.07054618746042252, + 0.0025260779075324535, + -0.04007257893681526, + 0.013598357327282429, + 0.03940897434949875, + -0.0069704861380159855, + 0.057934146374464035, + 0.15386416018009186, + 0.04337097704410553, + 0.07119690626859665, + -0.048173222690820694, + -0.09069827198982239, + -0.016886970028281212, + -0.04425429925322533, + -0.019464140757918358, + -0.027470778673887253, + 0.005336642265319824, + -0.09170512855052948, + 0.03556380048394203, + 0.023559197783470154, + -0.03628315031528473, + -0.04448218643665314, + 0.011364061385393143, + 0.009603296406567097, + -0.04861818626523018, + -0.03343017399311066, + -0.01483147218823433, + 0.06086479872465134, + 0.02109363302588463, + -0.025959225371479988, + 0.014000430703163147, + -0.00846729427576065, + 0.07305800914764404, + 0.02457829751074314, + -0.12663941085338593, + 0.010544337332248688, + 0.013315590098500252, + 0.07280771434307098, + -0.08232685923576355, + 0.0040486594662070274, + -0.026350753381848335, + 0.06408613175153732, + -0.01415738184005022, + 0.04628903791308403, + -0.02050374448299408, + 0.04177685081958771, + -0.09207800775766373, + -0.005421833600848913, + -0.005136478692293167, + -0.024564260616898537, + 0.026354163885116577, + -0.05851329490542412, + 0.03147275745868683, + -0.02183554694056511, + 0.03346295654773712, + -0.02240697667002678, + -0.09603817760944366, + -0.02274233102798462, + -0.039677977561950684, + 0.007695100735872984, + 0.039304088801145554, + -0.017668871209025383, + 0.022897064685821533, + -0.039273541420698166, + 0.08864572644233704, + -0.04432578384876251, + -0.06769558042287827, + 0.06696884334087372, + 0.07118263095617294, + -0.024863263592123985, + 0.01151553075760603, + -0.11591693758964539, + -0.025131937116384506, + 0.052269868552684784, + -0.03035089746117592, + 0.00906881783157587, + 0.04585501551628113, + 0.038361817598342896, + 0.03026638925075531, + 0.015340524725615978, + -0.006911538541316986, + 0.022395918145775795, + 0.13969141244888306, + 0.047686025500297546, + 0.05438247323036194, + 0.02779674343764782, + -0.04191797226667404, + -0.021741557866334915, + 0.003305602353066206, + -0.11100355535745621, + 0.016258426010608673, + 0.06977421790361404, + 0.08189859241247177, + 0.0966871827840805, + 0.03519754856824875, + 0.05674563720822334, + 0.034512776881456375, + 0.07052291929721832, + -0.06342307478189468, + 0.051868196576833725, + -0.013776403851807117, + -0.007541927974671125, + -0.043183840811252594, + 0.021481933072209358, + 0.0380198135972023, + -0.07870583236217499, + -0.10873759537935257, + -0.08491706103086472, + 0.03155837208032608, + -0.03790571913123131, + 0.041968367993831635, + -0.00593406381085515, + 0.036538854241371155, + -0.004705581348389387, + 0.004229994490742683, + -0.00729013979434967, + -0.019296232610940933, + -0.014331319369375706, + -4.401502220942261e-34, + -0.0067550260573625565, + 0.07402423769235611, + -0.012888211756944656, + -0.055266380310058594, + 0.04810081049799919, + 0.005175809375941753, + -0.004325157031416893, + -0.10392399877309799, + -0.03650582954287529, + 0.07477248460054398, + 0.0022102247457951307, + -0.05040738359093666, + -0.003033560933545232, + 0.060498371720314026, + 0.08619660884141922, + -0.04577762633562088, + -0.10468175262212753, + -0.07177772372961044, + -0.05756700038909912, + -0.02839704230427742, + -0.028650879859924316, + -0.010213681496679783, + 0.008074316196143627, + 0.03448071703314781, + -0.025478240102529526, + -0.029753824695944786, + 0.05397271364927292, + -0.0006062929751351476, + -0.03292117267847061, + 0.040799956768751144, + -0.0933983102440834, + -0.026921836659312248, + 0.00327915046364069, + -0.025635670870542526, + -0.057946983724832535, + -0.06664302200078964, + 0.04280361905694008, + -0.027111517265439034, + -0.08359260112047195, + 0.03483080118894577, + 0.023318039253354073, + -0.08598511666059494, + -0.08378149569034576, + 0.054067932069301605, + -0.014853178523480892, + -0.05498708039522171, + 0.0711284726858139, + 0.12539872527122498, + -0.04355800896883011, + -0.027553806081414223, + -0.009111037477850914, + -0.058482203632593155, + 0.07053986191749573, + -0.009027705527842045, + -0.017481567338109016, + 0.011404255405068398, + 0.06084389239549637, + -0.028110956773161888, + -0.08594057708978653, + 0.05215488001704216, + -0.07651112973690033, + -0.027076181024312973, + -0.01357623003423214, + -0.01263132132589817, + -0.0193876251578331, + 0.013576658442616463, + 0.038156066089868546, + -0.04309772700071335, + -0.04051121696829796, + -0.025885144248604774, + -0.003073574509471655, + -0.0003303807170595974, + 0.08043289929628372, + -0.039484549313783646, + 0.10091833025217056, + -0.04735022410750389, + 0.027813943102955818, + -0.0038837436586618423, + -0.05234759673476219, + -0.00716474698856473, + 0.016360750421881676, + -0.025806615129113197, + -0.03212691470980644, + -0.08456144481897354, + -0.019326699897646904, + -0.03228791803121567, + 0.07633069902658463, + -0.07643644511699677, + -0.03988131135702133, + 0.02396279387176037, + -0.055901359766721725, + 0.009231535717844963, + 0.0344320572912693, + 0.07486359030008316, + -0.03505600243806839, + -2.324423447670953e-34, + -0.04453577473759651, + 0.06512241810560226, + 0.03920532390475273, + 0.062222111970186234, + -0.015745285898447037, + -0.017774563282728195, + 0.08228200674057007, + -0.05798694118857384, + -0.042758435010910034, + -0.018822822719812393, + -0.07607664912939072, + -0.02666221559047699, + 0.036936040967702866, + -0.034714240580797195, + 0.06992944329977036, + 0.00530517753213644, + -0.0005260169273242354, + -0.03961028903722763, + 0.08799499273300171, + -0.04191635549068451, + 0.07468635588884354, + -0.010930310003459454, + -0.0611649826169014, + -0.04100184887647629, + 0.07131826132535934, + 0.03241356834769249, + -0.0545443594455719, + -0.005295638460665941, + -0.04712966829538345, + 0.032524388283491135, + -0.05130890756845474, + -0.01299980841577053, + 0.06523969769477844, + -0.011433755978941917, + 0.018730396404862404, + 0.047184932976961136, + -0.043041545897722244, + -0.03231072053313255, + -0.015864262357354164, + 0.03991076350212097, + -0.017617924138903618, + -0.03504975512623787, + 0.027346905320882797, + 0.05564267560839653, + 0.01610865257680416, + 0.0470576174557209, + -0.010647954419255257, + 0.13047614693641663, + -0.011055804789066315, + 0.011903814040124416, + -0.01350466813892126, + -0.0019897734746336937, + 0.053073540329933167, + 0.0717632919549942, + 0.007322370074689388, + -0.0206251572817564, + 0.061210062354803085, + 0.03184640407562256, + -0.035093698650598526, + -0.0026315131690353155, + -0.03291690722107887, + -0.04229205846786499, + -0.04241437837481499, + 0.1091129332780838, + 0.02229561097919941, + 0.02223002351820469, + 0.03949614241719246, + 0.031568314880132675, + -0.07121116667985916, + -0.07664268463850021, + -0.04235681891441345, + -0.011173299513757229, + 0.1190338209271431, + -0.09825095534324646, + -0.0375107042491436, + 0.007167852018028498, + 0.047537703067064285, + 0.044423725455999374, + 0.022106878459453583, + -0.02811007760465145, + 0.033864255994558334, + 0.0643145889043808, + 0.03725901246070862, + -0.0497952364385128, + -0.021733446046710014, + 0.023898839950561523, + -0.11254694312810898, + -0.06519465893507004, + 0.04424642026424408, + 0.09124527126550674, + 0.006083414424210787, + 0.09144245833158493, + 0.02653978019952774, + -0.01318738516420126, + 0.0480327382683754, + -3.0391063887691416e-08, + 0.051376331597566605, + -0.0002709411783143878, + -0.03103259764611721, + -0.018394535407423973, + 0.05002995952963829, + 0.05086217448115349, + -0.07317503541707993, + -0.0172730665653944, + -0.08379635214805603, + 0.1180257499217987, + 0.08445936441421509, + -0.025030585005879402, + -0.01965731382369995, + 0.046042654663324356, + 0.03724817931652069, + 0.028524605557322502, + 0.061249297112226486, + -0.027382537722587585, + -0.0011134583037346601, + -0.001871297718025744, + -0.04395337030291557, + 0.002261978341266513, + 0.06950556486845016, + -0.024213269352912903, + -0.0782783254981041, + -0.10320401936769485, + -0.022083906456828117, + -0.04333319142460823, + -0.0334695503115654, + 0.007842703722417355, + -0.03523677587509155, + 0.08107997477054596, + 0.00924254022538662, + -0.013395791873335838, + -0.019067300483584404, + -0.008446489460766315, + -0.1053837463259697, + 0.06697141379117966, + 0.06984667479991913, + 0.007155571132898331, + -0.038544610142707825, + 0.0132181691005826, + -0.004773592576384544, + -0.022143904119729996, + -0.09064015746116638, + -0.07600560784339905, + -0.042070601135492325, + -0.08189931511878967, + 0.03302472084760666, + 0.043238356709480286, + -0.01407547201961279, + -0.03778013586997986, + 0.030578600242733955, + 0.021573437377810478, + 0.04664295166730881, + 0.056082408875226974, + -0.07687672227621078, + -0.0018553169211372733, + -0.051700614392757416, + 0.043752558529376984, + -0.02636834792792797, + 0.05589277669787407, + 0.05282546952366829, + -0.016411008313298225 + ], + "model": "Jigger", + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))", + "price": 270, + "store_location": "-74.0060,40.7128" + }, + { + "brand": "Bicyk", + "condition": "used", + "description": "Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!", + "description_embeddings": [ + -0.004883771762251854, + 0.08099519461393356, + -0.022444017231464386, + 0.05437565594911575, + -0.07422323524951935, + 0.0066548739559948444, + -0.06268022209405899, + 0.042389899492263794, + -0.03745086118578911, + 0.058961447328329086, + 0.025613723322749138, + -0.04209878668189049, + 0.06861244142055511, + 0.01983577199280262, + 0.026353053748607635, + 0.045618414878845215, + 0.040685027837753296, + 0.09574265778064728, + 0.005801026243716478, + -0.027659950777888298, + -0.0223013274371624, + 0.040641166269779205, + 0.06608107686042786, + 0.0691058486700058, + -0.03629102557897568, + 0.035505786538124084, + -0.09211395680904388, + -0.011358118616044521, + -0.025078972801566124, + -0.017709167674183846, + 0.07587391883134842, + 0.08128049969673157, + 0.060521550476551056, + -0.0845090001821518, + -0.03779749944806099, + 0.030346086248755455, + 0.017926080152392387, + 0.003489845432341099, + -0.05622200667858124, + -0.06886664777994156, + -0.051538050174713135, + 0.029196197167038918, + -0.0028146395925432444, + 0.012419342994689941, + -0.06346380710601807, + -0.011617675423622131, + 0.04980290308594704, + -0.0799335315823555, + 0.016635078936815262, + 0.07064730674028397, + 0.04530491679906845, + -0.04372858256101608, + 0.07056037336587906, + -0.05052798613905907, + -0.01064316462725401, + -0.04754374921321869, + -0.08878123015165329, + 0.005363269243389368, + 0.032587066292762756, + -0.05610528588294983, + -0.0012875061947852373, + -0.03215320408344269, + -0.0045777312479913235, + -0.026692084968090057, + -0.09758491814136505, + -0.046251099556684494, + 0.03897765651345253, + -0.06587375700473785, + -0.013586618937551975, + -0.020807752385735512, + 0.023367363959550858, + 0.011167124845087528, + 0.003386110533028841, + -0.024887114763259888, + -0.029615335166454315, + -0.02571641281247139, + -0.03150812163949013, + -0.0395360104739666, + -0.049686528742313385, + -0.023117102682590485, + -0.07580453157424927, + 0.020851964130997658, + 0.0917917937040329, + -0.038357049226760864, + 0.05106140300631523, + -0.03367459401488304, + 0.05801103636622429, + 0.0628814697265625, + 0.024997225031256676, + 0.015594316646456718, + -0.01490987278521061, + 0.07070998847484589, + -0.039144083857536316, + 0.0657331570982933, + -0.053744420409202576, + -0.01675831526517868, + -0.008745769970119, + -0.07664742320775986, + -0.06751038879156113, + 0.0023392336443066597, + 0.018902592360973358, + 0.06754770874977112, + 0.07430258393287659, + 0.0806465670466423, + -0.031056180596351624, + 0.06557579338550568, + 0.06529161334037781, + -0.03742394223809242, + 0.0007822285988368094, + 0.10523669421672821, + 0.0038901956286281347, + -0.014934191480278969, + 0.0647430568933487, + 0.03438747301697731, + -0.046527378261089325, + 0.014247977174818516, + -0.020184241235256195, + 0.016480082646012306, + -0.05491460859775543, + 0.07232335209846497, + -0.016330908983945847, + 0.011368552222847939, + -0.001964043825864792, + 0.0009170984267257154, + 0.019140562042593956, + -0.002758787479251623, + -0.05629577115178108, + 1.7957766384702998e-33, + -0.10579885542392731, + 0.08271613717079163, + 0.03821941837668419, + 0.06078806892037392, + 0.017647448927164078, + -0.07404755055904388, + 0.06083450838923454, + -0.07097837328910828, + -0.01949647255241871, + -0.005204185377806425, + 0.0160058680921793, + -0.03624944016337395, + 0.065463587641716, + -0.04834574833512306, + 0.09314870834350586, + -0.022509299218654633, + -0.047614336013793945, + -0.042122796177864075, + 0.0014064351562410593, + 0.08215921372175217, + 0.0144058121368289, + -0.08526691794395447, + -0.01885370910167694, + 0.020506983622908592, + -0.0041589876636862755, + -0.0928102508187294, + 0.0965222716331482, + 0.05469893291592598, + 0.002785224001854658, + 0.006347258575260639, + -0.09394793212413788, + -0.08587668836116791, + 0.00999284815043211, + -0.015109539031982422, + 0.035454027354717255, + -0.08842843770980835, + -0.015698572620749474, + 0.05549640208482742, + -0.011119373142719269, + 0.012295924127101898, + 0.007523554377257824, + -0.03497130423784256, + -0.05309790000319481, + -0.021819932386279106, + 0.011010204441845417, + 0.0778549313545227, + 0.122015580534935, + 0.0451776348054409, + -0.0894458070397377, + 0.0031173918396234512, + -0.003828236600384116, + 0.0010151821188628674, + 0.0007775757694616914, + -0.0007406148943118751, + 0.0005911831394769251, + 0.029611686244606972, + -0.010095393285155296, + -0.015750357881188393, + -0.08871200680732727, + 0.06563369184732437, + 0.052563928067684174, + -0.02150006778538227, + 0.032858334481716156, + -0.039781685918569565, + -0.02986454777419567, + -0.017254218459129333, + 0.013349524699151516, + 0.04903600737452507, + -0.102760910987854, + 0.027411801740527153, + -0.007306735496968031, + 0.03547230362892151, + 0.03793823719024658, + -0.014224819839000702, + 0.004229242447763681, + -0.04914051666855812, + -0.05566011741757393, + -0.08426816016435623, + 0.0378078892827034, + -0.02177048847079277, + 0.037800222635269165, + 0.01145790982991457, + -0.03493969514966011, + -0.06417357921600342, + -0.04812582954764366, + -0.030254419893026352, + -0.12552082538604736, + 0.0017056012293323874, + -0.053679559379816055, + 0.019939688965678215, + -0.04766315221786499, + -0.143480584025383, + -0.024615854024887085, + 0.06507551670074463, + 0.01710103265941143, + -2.55080180279124e-33, + -0.01073896698653698, + 0.08023330569267273, + 0.028500312939286232, + -0.033364687114953995, + 0.018465891480445862, + -0.018969086930155754, + 0.116150863468647, + -0.04905116185545921, + 0.0067994301207363605, + -0.051097989082336426, + -0.047208935022354126, + 0.003005147911608219, + -0.006951641291379929, + 0.0299075860530138, + 0.023957515135407448, + 0.005555577110499144, + -0.020836569368839264, + 0.013542957603931427, + 0.09286782890558243, + -0.04009733721613884, + 0.05567550286650658, + 0.01991306059062481, + -0.16575683653354645, + -0.003300475887954235, + 0.11635366082191467, + 0.008300523273646832, + -0.1112738847732544, + 0.05307481065392494, + 0.009467027150094509, + 0.11263766884803772, + -0.04102758690714836, + -0.0505208782851696, + 0.1890914887189865, + -0.01593983918428421, + 0.011381726711988449, + 0.01095605455338955, + -0.08038999140262604, + -0.012621873058378696, + -0.005316049326211214, + 0.017261112108826637, + 0.03283751755952835, + -0.04533768445253372, + 0.03397509828209877, + 0.04211656376719475, + 0.024692395702004433, + -0.02541458234190941, + 0.02313675545156002, + 0.02338019199669361, + -0.011879520490765572, + -0.05438990890979767, + 0.03806900233030319, + 0.01261812262237072, + 0.02512892708182335, + -0.0028746703173965216, + -0.016077643260359764, + -0.032072994858026505, + -0.006427581422030926, + 0.01777057908475399, + 0.02934812381863594, + -0.05759994685649872, + -2.871774631785229e-05, + -0.03137838840484619, + -0.06273766607046127, + 0.04409930482506752, + -0.05993351340293884, + 0.007546861190348864, + 0.0053585791029036045, + 0.042325496673583984, + -0.007369876839220524, + 0.04489513114094734, + -0.12103329598903656, + 0.017391694709658623, + 0.0304956566542387, + -0.034047987312078476, + 0.02484256401658058, + -0.06834809482097626, + 0.06508748978376389, + 0.08324999362230301, + -0.020252887159585953, + -0.014722783118486404, + 0.02126440405845642, + -0.05160334333777428, + 0.045947108417749405, + 0.022960059344768524, + 0.023375188931822777, + -0.060902271419763565, + -0.05150751397013664, + -0.1094929575920105, + -0.04899677261710167, + 0.09132419526576996, + 0.051848214119672775, + -0.0077659315429627895, + 0.0012422297149896622, + 0.058530740439891815, + 0.040777210146188736, + -3.354356081786136e-08, + 0.025891084223985672, + -0.04088461399078369, + -0.06885679066181183, + 0.01951301097869873, + 0.047974348068237305, + 0.04472370818257332, + 0.004657004959881306, + 0.001041706302203238, + -0.02763887122273445, + 0.03814717009663582, + 0.03166148066520691, + 0.0063626947812736034, + 0.09577886760234833, + 0.06234167888760567, + 0.0010398400481790304, + 0.010609040968120098, + 0.020408503711223602, + 0.05596008151769638, + 0.00923844799399376, + 0.011290326714515686, + 0.02393697388470173, + -0.03378620743751526, + 0.010788901709020138, + 0.0072112190537154675, + -0.03552679717540741, + -0.10475718975067139, + 0.003995304461568594, + -0.002284976886585355, + -0.014504319056868553, + -0.06887608021497726, + 0.03398992121219635, + -0.005206231493502855, + 0.049566611647605896, + 0.00902023445814848, + 0.06874160468578339, + 0.014804325066506863, + -0.07230424880981445, + 0.0428827665746212, + 0.013657039031386375, + 0.027973631396889687, + -0.035619381815195084, + 0.06485525518655777, + -0.06238642707467079, + -0.012459578923881054, + 0.020500177517533302, + -0.0715484470129013, + -0.16523504257202148, + 0.013638298027217388, + 0.07008316367864609, + 0.026970835402607918, + 0.004871702753007412, + -0.0012540861498564482, + -0.028708957135677338, + 0.05812879279255867, + 0.12611250579357147, + 0.09877888858318329, + -0.04118988662958145, + -0.02214396744966507, + -0.10328112542629242, + 0.029945021495223045, + 0.004513312131166458, + 0.011272193863987923, + 0.03294430673122406, + -0.042709026485681534 + ], + "model": "Hillcraft", + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, -118.2887 33.9872, -118.2887 34.0972))", + "price": 1200, + "store_location": "-118.2437,34.0522" + }, + { + "brand": "Nord", + "condition": "used", + "description": "The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.", + "description_embeddings": [ + -0.0018494834657758474, + 0.057690851390361786, + 0.038153987377882004, + 0.06570218503475189, + 0.028856752440333366, + -0.062333013862371445, + -0.014953209087252617, + 0.046022992581129074, + -0.08184631168842316, + 0.03648914024233818, + 0.03869181126356125, + 0.010564669035375118, + -0.020310621708631516, + -0.04062078520655632, + -0.0125962495803833, + 0.14169928431510925, + 0.03418859466910362, + -0.06641822308301926, + 0.005633663386106491, + -0.09943458437919617, + 0.023237863555550575, + -0.04369724169373512, + 0.016574522480368614, + 0.07258585095405579, + 0.018674470484256744, + -0.05764088034629822, + -0.0795072391629219, + 0.04034125804901123, + -0.036483921110630035, + -0.033106740564107895, + 0.02980787307024002, + -0.0028512384742498398, + 0.00786224752664566, + -0.03016488254070282, + -0.12349128723144531, + 0.031072411686182022, + 0.08362030982971191, + 0.025227056816220284, + -0.030982907861471176, + -0.006486377213150263, + -0.023590318858623505, + -0.03374557942152023, + -0.04145599156618118, + -0.09421771764755249, + -0.0013142612297087908, + -0.003397064981982112, + -0.0031338112894445658, + -0.11464792490005493, + 0.040542472153902054, + 0.02896481193602085, + 0.007327641360461712, + -0.06064218282699585, + 0.049546848982572556, + -0.05917377769947052, + -0.01963184028863907, + -0.002139812568202615, + -0.14361988008022308, + -0.05401389300823212, + 0.12506668269634247, + -0.07141950726509094, + -0.0032961040269583464, + 0.015251584351062775, + -0.05507654324173927, + -0.009836667217314243, + -0.02802908606827259, + -0.01053905300796032, + -0.03239851072430611, + -0.10646941512823105, + 0.03140658512711525, + 0.028125958517193794, + -0.004000179003924131, + -0.0018343725241720676, + 0.01727917790412903, + -0.013935663737356663, + -0.02435036562383175, + 0.04411163926124573, + -0.009158330969512463, + -0.023309389129281044, + 0.01229795441031456, + -0.04689493402838707, + -0.02138197235763073, + 0.013063939288258553, + 0.02832808345556259, + 0.031972818076610565, + 0.020882662385702133, + -0.015083174221217632, + 0.002903456799685955, + -0.047304242849349976, + -0.10658963769674301, + -0.06274145841598511, + -0.030370736494660378, + 0.0539257749915123, + 0.00848578754812479, + 0.02172423154115677, + -0.007691903971135616, + -0.10581931471824646, + 0.06078812852501869, + 0.007988635450601578, + -0.116583913564682, + 0.05237157270312309, + 0.024445366114377975, + -0.02832716703414917, + 0.04004029557108879, + 0.02844531089067459, + -0.0455174520611763, + -0.07379734516143799, + 0.09453199058771133, + -0.011599121615290642, + -0.0027194281574338675, + 0.014355232007801533, + -0.059690698981285095, + -0.012937567196786404, + 0.034315045922994614, + -0.047598548233509064, + -0.03261064738035202, + 0.05839747563004494, + -0.12691465020179749, + 0.03778312727808952, + 0.006131120957434177, + 0.04806787148118019, + -0.03441976010799408, + 0.08042734116315842, + 0.008934197947382927, + 0.027216315269470215, + 0.0016972541343420744, + -0.10113930702209473, + -0.0003218930505681783, + 9.75532108829862e-34, + -0.0537416972219944, + 0.06875170767307281, + -0.01566525548696518, + 0.01952524110674858, + -0.0005404680850915611, + -0.08984242379665375, + 0.04537447541952133, + -0.1295408457517624, + 7.603532867506146e-05, + 0.040753066539764404, + 0.016371281817555428, + 0.029906686395406723, + -0.005372706335037947, + -0.06687828153371811, + 0.11607439070940018, + 0.016209086403250694, + -0.11238335072994232, + -0.057236358523368835, + -0.09619198739528656, + 0.027146028354763985, + -0.09542766213417053, + -0.0360424630343914, + -0.09913153201341629, + 0.04242968559265137, + 0.05494842678308487, + -0.025294257327914238, + 0.06307625770568848, + 0.007745219860225916, + -0.019641151651740074, + 0.07056662440299988, + -0.05425839126110077, + 0.012385660782456398, + -0.006104010157287121, + -0.07186716794967651, + -0.10919132828712463, + -0.0017968777101486921, + 0.010471112094819546, + -0.011221444234251976, + -0.035078633576631546, + 0.009300827980041504, + 0.0802159234881401, + -0.10042990744113922, + -0.01718892529606819, + 0.05525779724121094, + -0.019847391173243523, + 0.08405174314975739, + 0.06403975188732147, + 0.0627448782324791, + -0.07871688157320023, + 0.0002676364383660257, + -0.05110163986682892, + 0.006078454665839672, + -0.019803548231720924, + -0.014253707602620125, + 0.039836447685956955, + 0.021812820807099342, + 0.00014793995069339871, + -0.006739508826285601, + -0.023166682571172714, + 0.05429920181632042, + 0.1337796151638031, + -0.01773720420897007, + 0.024304436519742012, + -0.016411837190389633, + -0.07777299731969833, + 0.042669400572776794, + 0.06226645037531853, + -0.023803485557436943, + 0.00396164134144783, + -0.049458179622888565, + -0.004774407483637333, + 0.036529600620269775, + 0.02002328634262085, + -0.02465248480439186, + -0.024076614528894424, + -0.03887653350830078, + 0.057176534086465836, + -0.03888818621635437, + -0.027698175981640816, + 0.001469603506848216, + 0.04755152389407158, + -0.0938708707690239, + 0.013991769403219223, + -0.026987746357917786, + -0.03277081623673439, + -0.04837489873170853, + 0.02711542509496212, + -0.09185922145843506, + -0.03453921154141426, + 0.03532274439930916, + -0.024472877383232117, + -0.09732313454151154, + 0.008513586595654488, + 0.03207352012395859, + 0.030766190961003304, + -2.5405224720352836e-33, + 0.08067575097084045, + -0.008160247467458248, + 0.03101508319377899, + 0.005022854544222355, + 0.0010000152979046106, + 0.06958161294460297, + 0.10594375431537628, + -0.06177408620715141, + -0.01650322414934635, + 0.03043895959854126, + -0.018333615735173225, + 0.06436911225318909, + 0.02439672127366066, + -0.01213911734521389, + 0.09937868267297745, + 0.02093592658638954, + -0.003140130080282688, + -0.016580305993556976, + 0.10430759936571121, + -0.019038936123251915, + 0.05574416741728783, + 0.07455366849899292, + -0.029936065897345543, + 0.0017704741330817342, + 0.07022519409656525, + 0.02108696848154068, + -0.050372399389743805, + 0.027957700192928314, + 0.005698348395526409, + 0.03494682535529137, + -0.02610155940055847, + -0.0068950653076171875, + 0.1346345692873001, + 0.03947756066918373, + -0.05966781824827194, + -0.010489783249795437, + -0.04099087417125702, + -0.027186688035726547, + -0.04620056599378586, + -0.02478279173374176, + -0.007060651201754808, + -0.023514581844210625, + 0.06082035228610039, + -0.05849218741059303, + -0.0036981222219765186, + 0.04169313609600067, + 0.09352244436740875, + 0.020741308107972145, + -0.0019505824893712997, + -0.10489305853843689, + 0.08789870887994766, + 0.039999835193157196, + -0.014334832318127155, + 0.008347390219569206, + 0.03113699145615101, + 0.10894497483968735, + 0.027165820822119713, + 0.0064145540818572044, + -0.00803150050342083, + -0.055484138429164886, + -0.03251631557941437, + 0.02290980890393257, + 0.04825572296977043, + 0.01608354039490223, + -0.04969468340277672, + -0.004120110999792814, + -0.03278858959674835, + 0.009696378372609615, + -0.04376300796866417, + -0.0009336083312518895, + -0.0313178189098835, + -0.05882050469517708, + 0.057815250009298325, + -0.02050788328051567, + -0.024381538853049278, + 0.06283771246671677, + 0.06954411417245865, + 0.09720556437969208, + -0.056403111666440964, + -0.04526498168706894, + -0.07259991019964218, + 0.001844316371716559, + 0.04090375825762749, + 0.06941819936037064, + -0.041316937655210495, + 0.002292247023433447, + -0.03425106778740883, + -0.0628972053527832, + 0.0063382769003510475, + 0.09668626636266708, + 0.062228571623563766, + 0.03658486530184746, + -0.08789398521184921, + 0.0009696391643956304, + -0.004108588211238384, + -3.183854957455878e-08, + 0.09346635639667511, + 0.042943451553583145, + -0.0005091542261652648, + 0.026524608954787254, + 0.009858721867203712, + 0.03737989068031311, + -0.014056878164410591, + 0.038327474147081375, + -0.010239921510219574, + 0.05757640674710274, + 0.014411918818950653, + 0.038742948323488235, + 0.06475342065095901, + -0.0011537548853084445, + -0.017729472368955612, + 0.05167960748076439, + 0.024419916793704033, + 0.08681508898735046, + 0.015235288999974728, + 0.036676522344350815, + -0.03421042487025261, + -0.019762659445405006, + 0.09467294812202454, + -0.053133491426706314, + -0.04356615990400314, + -0.03919585049152374, + 0.015591761097311974, + 0.021372869610786438, + -0.0058142030611634254, + -0.022864477708935738, + -0.02901598811149597, + 0.0458187572658062, + -0.030826766043901443, + -0.008975986391305923, + 0.03365883231163025, + -0.010484383441507816, + -0.1445801854133606, + 0.05828193947672844, + -0.028194306418299675, + 0.05975533276796341, + 0.014028828591108322, + 0.0036430624313652515, + 0.024983061477541924, + 0.01454286277294159, + -0.006972659844905138, + 0.037446193397045135, + -0.06949692964553833, + -0.09630566090345383, + 0.03263894096016884, + 0.048720087856054306, + -0.06886433064937592, + 0.018142051994800568, + 0.03894415125250816, + 0.05843216925859451, + 0.06860719621181488, + 0.04971907287836075, + -0.025701280683279037, + -0.06293400377035141, + -0.05422094464302063, + 0.01912975125014782, + 0.009564549662172794, + 0.055643752217292786, + -0.0027948219794780016, + 0.0329461470246315 + ], + "model": "Chook air 5", + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, -87.6848 41.8231, -87.6848 41.9331))", + "price": 815, + "store_location": "-87.6298,41.8781" + }, + { + "brand": "Eva", + "condition": "used", + "description": "The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It\u2019s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!", + "description_embeddings": [ + 0.035103436559438705, + 0.02666405402123928, + -0.06364161521196365, + 0.02551678754389286, + -0.005281867925077677, + -0.04041111096739769, + -0.020820949226617813, + -0.03485208749771118, + -0.07683732360601425, + 0.021527189761400223, + 0.007516156416386366, + -0.0034474905114620924, + 0.030465560033917427, + -0.06572654843330383, + -0.020914506167173386, + 0.032637566328048706, + 0.0815017819404602, + -0.022764643654227257, + 0.07385077327489853, + 0.058004263788461685, + -0.03062591142952442, + -0.03927106410264969, + 0.01410673838108778, + 0.05480317771434784, + -0.05647570267319679, + -0.008725483901798725, + -0.05373937636613846, + 0.03546926751732826, + -0.026455262675881386, + -0.09718386828899384, + -0.040623344480991364, + 0.03622450679540634, + 0.08971034735441208, + -0.038574062287807465, + -0.05105822533369064, + 0.021020587533712387, + 0.003312851767987013, + -0.008969941176474094, + -0.0568903312087059, + 0.033502694219350815, + -0.0578635074198246, + -0.03721226751804352, + -0.036668986082077026, + 0.0022641047835350037, + 0.038053013384342194, + 0.03350543603301048, + 0.05258995667099953, + -0.007619654294103384, + -0.05145309492945671, + 0.026286069303750992, + 0.09238694608211517, + -0.06733417510986328, + 0.05791507661342621, + -0.07623173296451569, + -0.0052175214514136314, + -0.04393303394317627, + -0.15249019861221313, + -0.019764462485909462, + -0.03331150859594345, + -0.11831824481487274, + 0.05683637782931328, + 0.01903577335178852, + -0.023615414276719093, + 0.04120348393917084, + -0.05669170990586281, + 0.002014430705457926, + 0.009953595697879791, + 0.008390115574002266, + -0.025982441380620003, + -0.05977262556552887, + 0.0611012764275074, + 0.010146236047148705, + -0.047625984996557236, + 0.09061623364686966, + 0.02551751770079136, + 0.06041749566793442, + 0.1113404780626297, + 0.017673874273896217, + -0.05184035003185272, + 0.023109234869480133, + -0.04408435523509979, + -0.09029985964298248, + 0.06733208149671555, + 0.049696795642375946, + 0.05554559826850891, + 0.01563390903174877, + -0.019527558237314224, + -0.014796985313296318, + 0.003595865098759532, + 0.012917418964207172, + -0.06858907639980316, + -0.008697138167917728, + 0.04721860587596893, + 0.03153855353593826, + -0.05275731533765793, + 0.019923491403460503, + -0.009707989171147346, + -0.018920045346021652, + 0.03773808106780052, + 0.042773403227329254, + -0.03819388151168823, + 0.04029008001089096, + 0.060834936797618866, + 0.07531940191984177, + -0.04695741459727287, + 0.00488079572096467, + 0.13518987596035004, + -0.014429144561290741, + 0.0314130075275898, + 0.04135369136929512, + -0.021350333467125893, + -0.01289785373955965, + 0.032701168209314346, + -0.037027571350336075, + -0.018168980255723, + -0.07188623398542404, + -0.036501020193099976, + 0.03303954005241394, + 0.10676111280918121, + -0.03277475759387016, + -0.06245853006839752, + -0.011879103258252144, + 0.0533418171107769, + -0.01446340698748827, + 0.03530410677194595, + 0.05820532143115997, + -0.0015766610158607364, + -8.477015062817539e-34, + -0.10837127268314362, + 0.015582970343530178, + 0.0182977095246315, + 0.055213626474142075, + -0.03584470599889755, + 0.02778925560414791, + 0.08698058873414993, + -0.04999695345759392, + -0.0842076763510704, + -0.040448326617479324, + -0.06311061233282089, + 0.02668358013033867, + 0.04676111042499542, + -0.006417605560272932, + 0.12788759171962738, + 0.02197197638452053, + 0.07035308331251144, + -0.04392284154891968, + -0.008788101375102997, + -0.001108768628910184, + 0.07960236072540283, + -0.0019975434988737106, + -0.010499294847249985, + 0.03599945828318596, + 0.02329486794769764, + -0.01962442137300968, + 0.15486162900924683, + 0.027215363457798958, + -0.022036811336874962, + 0.04227740690112114, + -0.07894206047058105, + -0.03180933743715286, + 0.020022651180624962, + -0.05859709531068802, + -0.0009564562351442873, + 0.009732870385050774, + -0.09007531404495239, + 0.041766807436943054, + 0.003291067900136113, + 0.0950121283531189, + 0.02693203091621399, + -0.01629827171564102, + -0.01884087361395359, + -0.029889501631259918, + -0.003714299062266946, + 0.09247232973575592, + 0.06151247024536133, + 0.09120925515890121, + -0.011266379617154598, + 0.03680066391825676, + -0.1628604233264923, + -0.008045083843171597, + -0.023876454681158066, + 0.046465914696455, + 0.01841152086853981, + 0.01759498566389084, + -0.012628139927983284, + 0.02123965509235859, + -0.07098977267742157, + 0.025028834119439125, + -0.032171521335840225, + 0.0538402758538723, + -0.00211805896833539, + -0.019548164680600166, + -0.046793293207883835, + 0.04394293949007988, + 0.022090770304203033, + -0.058215122669935226, + -0.05101722851395607, + 0.014057288877665997, + -0.05860457569360733, + 0.006166193168610334, + 0.04641130194067955, + 0.03165149688720703, + 0.045289356261491776, + 0.038580797612667084, + -0.0335664264857769, + 0.013378577306866646, + 0.058321163058280945, + -0.03730795532464981, + -0.01996016688644886, + 0.025774186477065086, + 0.001352976425550878, + 0.023636724799871445, + 0.026727410033345222, + -0.0730309784412384, + -0.01613243669271469, + -0.009525856003165245, + -0.03985843434929848, + -0.003976964857429266, + -0.006743384525179863, + -0.06324627995491028, + 0.03959967568516731, + 0.021898096427321434, + -0.004184887744486332, + -3.110083923319741e-34, + 0.03726857528090477, + -0.016945818439126015, + 0.05512943118810654, + 0.0471712127327919, + 0.097488172352314, + 0.02456829510629177, + 0.08276744186878204, + -0.02230781689286232, + -0.03785759210586548, + 0.010082835331559181, + 0.07151538133621216, + -0.04426267370581627, + 0.03402319550514221, + 0.05808456242084503, + 0.07566764950752258, + 0.07006501406431198, + 0.002404613886028528, + -0.10078012198209763, + 0.0029093879275023937, + -0.12370351701974869, + -0.03431423008441925, + 0.08334372937679291, + 0.001441179309040308, + -0.08001153916120529, + 0.020076438784599304, + 0.011177998036146164, + 0.034560561180114746, + 0.041609298437833786, + -0.04921843111515045, + 0.03598331660032272, + -0.10671709477901459, + 0.02969883382320404, + 0.048317357897758484, + 0.12375228852033615, + 0.040479566901922226, + 0.0330270379781723, + -0.06994733214378357, + 0.008698385208845139, + 0.03447363153100014, + -0.012882913462817669, + -0.02672695368528366, + -0.06423910707235336, + -0.0506032295525074, + 0.0053747911006212234, + 0.0682206079363823, + -0.005316274706274271, + 0.027013784274458885, + -0.006269444711506367, + 0.08528510481119156, + -0.0731915608048439, + 0.0046186731196939945, + -0.024244142696261406, + 0.0360478051006794, + 0.020591434091329575, + 0.04302601516246796, + -0.14392279088497162, + 0.05873512104153633, + -0.010028650052845478, + -0.04548479616641998, + -0.013535127975046635, + 0.01113649271428585, + 0.028603754937648773, + -0.04036158323287964, + 0.0704532265663147, + -0.09373150765895844, + -0.07198052108287811, + -0.002203285926952958, + -0.0855080857872963, + -0.08842256665229797, + -9.343179408460855e-05, + -0.029457710683345795, + -0.02197202481329441, + -0.048936717212200165, + 0.06165122613310814, + -0.0626755878329277, + 0.009602922946214676, + 0.07251632958650589, + 0.03113914094865322, + -0.0010769155342131853, + -0.040936876088380814, + -0.04328843951225281, + -0.020654935389757156, + 0.08143945783376694, + 0.03210373967885971, + 0.03849901258945465, + 0.08388370275497437, + -0.04882088676095009, + -0.04764509201049805, + -0.01594746671617031, + 0.08005915582180023, + -0.0007682700525037944, + 0.04356953501701355, + -0.08784972876310349, + -0.009003485552966595, + -0.012332507409155369, + -3.9398202034135466e-08, + 0.006421600468456745, + 0.04334808140993118, + 0.0013437344459816813, + -0.009495558217167854, + -0.022657591849565506, + -0.012704327702522278, + 0.018703341484069824, + -0.07545771449804306, + -0.07807240635156631, + 0.0031038280576467514, + -0.023932253941893578, + 0.040701914578676224, + 0.10603249073028564, + 0.03773405775427818, + 0.05925403907895088, + -0.02083730883896351, + 0.052759915590286255, + 0.08195225894451141, + -0.057340435683727264, + -0.03538660705089569, + 0.04492119327187538, + -0.07101765275001526, + 0.02868317998945713, + -0.11165601015090942, + -0.03126508370041847, + -0.07953942567110062, + -0.022893071174621582, + -0.08839578926563263, + 0.00727836275473237, + -0.08685773611068726, + 0.026177138090133667, + 0.028831886127591133, + 0.03626682609319687, + -0.045381225645542145, + -0.013574030250310898, + 0.028583087027072906, + -0.004301569424569607, + 0.04326719045639038, + -0.023370176553726196, + 0.057897310703992844, + -0.029195263981819153, + -0.001789534231647849, + 0.032133687287569046, + 0.003419605316594243, + 0.028055502101778984, + -0.0038284522015601397, + -0.022835813462734222, + -0.07568305730819702, + 0.017481982707977295, + 0.013085611164569855, + 0.009357997216284275, + -0.030347391963005066, + -0.0020967889577150345, + 0.03544173017144203, + 0.004640925209969282, + 0.025202661752700806, + -0.0916307270526886, + -0.06341513246297836, + -0.017053432762622833, + 0.06747056543827057, + 0.0467485710978508, + -0.14364545047283173, + 0.024641912430524826, + -0.04414863884449005 + ], + "model": "Eva 291", + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))", + "price": 3400, + "store_location": "-80.1918,25.7617" + }, + { + "brand": "Noka Bikes", + "condition": "used", + "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women\u2019s saddle, different bars and unique colourway.", + "description_embeddings": [ + 0.07420087605714798, + -0.01575474441051483, + 0.010670951567590237, + 0.07449527084827423, + 0.0060751838609576225, + 0.03211340680718422, + -0.008086569607257843, + 0.025549469515681267, + -0.07154879719018936, + 0.014914358966052532, + -0.030715830624103546, + -0.0064881909638643265, + -0.0071744490414857864, + -0.0495535172522068, + 0.03856685757637024, + 0.056132566183805466, + 0.10794447362422943, + -0.010181701742112637, + 0.0811777114868164, + 0.10135157406330109, + -0.0769873708486557, + -0.019640743732452393, + -0.028702793642878532, + 0.07654724270105362, + -0.07045057415962219, + -0.10104052722454071, + -0.004679638426750898, + -0.0027665267698466778, + 0.030919065698981285, + -0.05461210012435913, + -0.020669877529144287, + 0.031864751130342484, + 0.0835040882229805, + -0.05930671840906143, + -0.006361816544085741, + -0.044132985174655914, + 0.035945694893598557, + -0.0027891425415873528, + -0.07598478347063065, + -0.03105698712170124, + -0.059601303189992905, + -0.018435664474964142, + -0.03314891457557678, + 0.06877987831830978, + -0.016064301133155823, + 0.01545438077300787, + 0.056307148188352585, + -0.08382677286863327, + -0.05274924635887146, + 0.01139853335916996, + 0.10750263929367065, + -0.004042057786136866, + 0.003356053726747632, + -0.01083006989210844, + -0.06153054162859917, + -0.054990023374557495, + -0.1360272616147995, + 0.025806479156017303, + 0.043233320116996765, + -0.009591769427061081, + 0.04422037675976753, + 0.02436559833586216, + -0.0012137923622503877, + -0.015348327346146107, + 0.02425999939441681, + -0.03409525752067566, + -0.08487176895141602, + 0.017345253378152847, + 0.035498712211847305, + -0.05615586042404175, + 0.02745831571519375, + 0.04017677903175354, + -0.0936250239610672, + 0.048734042793512344, + 0.024287866428494453, + -0.030274393036961555, + 0.08800201117992401, + 0.0879313051700592, + -0.0550895519554615, + -0.0026407642289996147, + -0.09552258998155594, + -0.05870680510997772, + 0.09009534120559692, + 0.05674433708190918, + 0.05360940843820572, + -0.0023017164785414934, + 0.005036584101617336, + -0.017987212166190147, + -0.05129106715321541, + -0.0012550759129226208, + -0.05818512290716171, + 0.06725005060434341, + 0.02540011890232563, + 0.054589059203863144, + 0.01723511517047882, + -0.04752320423722267, + 0.10586173087358475, + 0.05530202388763428, + -0.03287632390856743, + 0.05258375406265259, + 0.01655220240354538, + 0.011849797330796719, + 0.032285332679748535, + 0.03760116547346115, + -0.017522728070616722, + -0.05457543954253197, + 0.006817341782152653, + -0.028400525450706482, + -0.0027318967040628195, + -0.04200681298971176, + -0.026171568781137466, + -0.03272707015275955, + 0.016305547207593918, + 0.01545319240540266, + 0.0056663742288947105, + -0.0977277085185051, + -0.0009383464348502457, + -0.005693590268492699, + 0.039240892976522446, + 0.01645808108150959, + -0.01950509287416935, + 0.007762903813272715, + -0.004406424704939127, + 0.016364052891731262, + -0.006125410553067923, + -0.029666034504771233, + 0.044946398586034775, + 1.8357034061219326e-33, + -0.053835414350032806, + 0.03495992720127106, + -0.05977822467684746, + -0.009059705771505833, + 0.004659880883991718, + 0.022712793201208115, + 0.04409413039684296, + -0.10094501823186874, + -0.08702167868614197, + 0.0066354661248624325, + 0.011878615245223045, + 0.0033176038414239883, + -0.012380938045680523, + 0.0010130798909813166, + 0.0644172728061676, + -0.07130810618400574, + -0.0045928386971354485, + -0.03375661373138428, + -0.022520871832966805, + 0.07504180818796158, + 0.009861051104962826, + 0.07191396504640579, + -0.014361198991537094, + 0.01449025422334671, + -0.010663183405995369, + -0.028888769447803497, + 0.09297508746385574, + 0.04386264830827713, + -0.09579017758369446, + 0.007718369830399752, + -0.11800525337457657, + -0.020133504644036293, + -0.0034684871789067984, + -0.03735410049557686, + 0.006975250784307718, + -0.012902614660561085, + 0.04201272875070572, + 0.04273228347301483, + -0.07133302837610245, + 0.09145014733076096, + 0.0048939259722828865, + -0.058044496923685074, + -0.017591776326298714, + -0.03502552956342697, + -0.05678534135222435, + 0.05471408739686012, + 0.09568087011575699, + 0.09486310184001923, + -0.045761823654174805, + 0.018188882619142532, + -0.12331638485193253, + -0.023129422217607498, + -0.05455191433429718, + 0.06732556223869324, + 0.048286039382219315, + -0.0014048831071704626, + 0.03308422118425369, + -0.009839850477874279, + -0.05661558359861374, + 0.03290552645921707, + 0.03661618381738663, + 0.0047610136680305, + -0.05254451557993889, + 0.006052205804735422, + -0.13153813779354095, + -0.006893055979162455, + 0.037368785589933395, + -0.03058353252708912, + -0.0017269864911213517, + -0.008851475082337856, + -0.0740399956703186, + 0.057353563606739044, + 0.12296456098556519, + 0.0413251556456089, + 0.09911972284317017, + 0.016817500814795494, + -0.048074446618556976, + 0.05658562108874321, + -0.012700358405709267, + -0.08663205802440643, + 0.0014393541496247053, + 0.00613820506259799, + -0.04446813836693764, + 0.04673916846513748, + -0.02757285162806511, + -0.05133393779397011, + -0.028082016855478287, + -0.04668722674250603, + -0.03849129378795624, + -0.047927819192409515, + -0.06503037363290787, + -0.08361010253429413, + 0.0393000952899456, + 0.04658354073762894, + 0.009724845178425312, + -2.0251938301584547e-33, + 0.07265108078718185, + -0.04307040572166443, + 0.04996086284518242, + 0.03520473092794418, + 0.1084771379828453, + 0.005833788774907589, + 0.046216338872909546, + -0.04042917117476463, + -0.04179416596889496, + 0.03598152473568916, + 0.04082870855927467, + -0.10612837225198746, + -0.010728210210800171, + 0.03610331192612648, + 0.027484672144055367, + -0.008130725473165512, + 0.026157474145293236, + -0.07420457899570465, + 0.025218356400728226, + -0.0898737907409668, + -0.02742879092693329, + 0.03535477817058563, + -0.062408916652202606, + -0.1569257229566574, + -0.018789052963256836, + 0.04845775291323662, + -0.015149657614529133, + -0.0018356313230469823, + -0.04590461030602455, + 0.023325689136981964, + -0.13069787621498108, + -0.01994006149470806, + -0.021577563136816025, + -0.0231519415974617, + -0.002478161361068487, + 0.022444186732172966, + -0.04681165888905525, + 0.02769998274743557, + 0.003438781714066863, + 0.03829822316765785, + 0.0243418887257576, + -0.020368436351418495, + -0.005010040942579508, + 0.04648580029606819, + 0.061260052025318146, + 0.03784928843379021, + 0.008547516539692879, + 0.027585946023464203, + 0.043608926236629486, + 0.047329846769571304, + 0.10433284938335419, + 0.0069401939399540424, + 0.043503858149051666, + 0.06499945372343063, + -0.01040438748896122, + -0.11596397310495377, + 0.06451629102230072, + 0.032956890761852264, + 0.0007023254293017089, + 0.0898808017373085, + -0.055529963225126266, + -0.006572310347110033, + -0.06089714914560318, + 0.022685443982481956, + -0.0013110691215842962, + -0.09440209716558456, + -0.03121936321258545, + -0.003408250631764531, + -0.08051110059022903, + 0.01802976056933403, + 0.032015636563301086, + -9.986010991269723e-05, + -0.011806532740592957, + 0.06818002462387085, + 0.028290249407291412, + -0.005312844179570675, + 0.06016719341278076, + 0.014397598803043365, + 0.07122596353292465, + 0.007864296436309814, + 0.03002011403441429, + -0.05413847416639328, + 0.09744291007518768, + -0.01965983398258686, + 0.04150940105319023, + 0.14307892322540283, + -0.11320102959871292, + 0.03503091260790825, + 0.003515399992465973, + -0.023677969351410866, + 0.03955819085240364, + 0.0534457303583622, + 0.01904974691569805, + 0.031267836689949036, + -0.018026702105998993, + -3.930426117904062e-08, + 0.0003057526773773134, + 0.013398992829024792, + -0.10113491117954254, + -0.016498351469635963, + -0.00534934364259243, + 0.017148228362202644, + -0.03217773512005806, + -0.030934104695916176, + -0.06334194540977478, + 0.07242853194475174, + -0.0047585368156433105, + -0.01996637135744095, + -0.0016463182400912046, + 0.01777566224336624, + -0.04115547239780426, + 0.02906038798391819, + 0.10273877531290054, + 0.022776247933506966, + 0.030065912753343582, + -0.014570299535989761, + 0.047994960099458694, + -0.044575825333595276, + 0.025753378868103027, + -0.002644166350364685, + -0.09429919719696045, + -0.11280428618192673, + 0.021306486800312996, + 0.010426397435367107, + 0.021093064919114113, + -0.00363176385872066, + -0.024409662932157516, + 0.11473581194877625, + 0.0020395806059241295, + -0.08371622860431671, + -0.03764123469591141, + 0.039440982043743134, + -0.022172994911670685, + 0.0410802997648716, + -0.05086303874850273, + 0.053952641785144806, + -0.06084167957305908, + -0.036132071167230606, + 0.02080748789012432, + -0.043120622634887695, + 0.04048219695687294, + 0.05917482078075409, + 0.04085611552000046, + -0.05118294060230255, + 0.015727631747722626, + 0.01915580965578556, + 0.03531794995069504, + -0.058007121086120605, + 0.08498086780309677, + 0.0036822939291596413, + 0.018125539645552635, + 0.10145474225282669, + -0.05804500728845596, + -0.008451723493635654, + -0.029611116275191307, + 0.008798911236226559, + -0.03220565244555473, + -0.05617227777838707, + 0.03482669219374657, + -0.03508705273270607 + ], + "model": "Kahuna", + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, -122.4644 37.7099, -122.4644 37.8199))", + "price": 3200, + "store_location": "-122.4194,37.7749" + }, + { + "brand": "Breakout", + "condition": "new", + "description": "The XBN 2.1 Alloy is our entry-level road bike \u2013 but that\u2019s not to say that it\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.", + "description_embeddings": [ + -0.03482655808329582, + -0.024290302768349648, + 0.029588153585791588, + -0.04068901762366295, + 0.006173150148242712, + -0.06071849912405014, + 0.021348219364881516, + 0.032668184489011765, + -0.12000397592782974, + -0.026135660707950592, + -0.0262606181204319, + -0.014927615411579609, + 0.06986956298351288, + -0.006424048915505409, + -0.002834598533809185, + -0.005902229342609644, + 0.07471967488527298, + -0.10197333246469498, + 0.08836513012647629, + 0.0504477359354496, + 0.005102860741317272, + 0.0359189435839653, + -0.0477643720805645, + 0.02106260135769844, + 0.030146656557917595, + 0.04048784077167511, + -0.023416390642523766, + -0.0016326266340911388, + 0.039820387959480286, + -0.04475116729736328, + -0.03422572463750839, + 0.09266746789216995, + -0.04106999188661575, + -0.02442021667957306, + -0.006325263995677233, + -0.057226430624723434, + 0.12074039876461029, + 0.012658749707043171, + -0.09748225659132004, + -0.048439353704452515, + -0.004550903104245663, + -0.014449959620833397, + 0.03684601932764053, + 0.018772389739751816, + 0.09577438980340958, + 0.04358510673046112, + 0.06987427175045013, + -0.0561334528028965, + -0.03684744983911514, + 0.02584502473473549, + 0.039536770433187485, + -0.061133887618780136, + 0.05351594462990761, + -0.025124184787273407, + 0.09431525319814682, + 0.05062168091535568, + -0.11715397238731384, + 0.044368330389261246, + -0.024262988939881325, + -0.024561986327171326, + 0.036067184060811996, + 0.025171849876642227, + 0.033648427575826645, + 0.046933531761169434, + 0.09548858553171158, + -0.06253167241811752, + -0.045195333659648895, + -0.04183578118681908, + -0.040385909378528595, + -0.027590686455368996, + 0.08240272104740143, + -0.04743463918566704, + 0.004482025280594826, + 0.03219064325094223, + 0.0007497097249142826, + -0.04202196002006531, + 0.10904812812805176, + -0.06721051037311554, + -0.023717032745480537, + -0.01718866638839245, + -0.11568725854158401, + 0.038704562932252884, + 0.04929516091942787, + -0.03311185538768768, + -0.0017465045675635338, + -0.06153444945812225, + 0.019559966400265694, + -0.010708335787057877, + -0.06623212993144989, + 0.03114122338593006, + 0.03581896796822548, + 0.05368824675679207, + 0.016093581914901733, + -0.0077278027310967445, + 0.025581376627087593, + 0.02375067211687565, + -0.024797597900032997, + 0.08371419459581375, + -0.023117447271943092, + 0.04535554349422455, + -0.007224411237984896, + 0.06131899356842041, + 0.02283627539873123, + -0.052097078412771225, + -0.07922855019569397, + 0.0016958570340648293, + 0.051785532385110855, + 0.06701217591762543, + -0.02913537621498108, + 0.020526740700006485, + 0.03941992297768593, + -0.02217993699014187, + -0.08051256835460663, + -0.10155311226844788, + -0.05453299731016159, + -0.06470758467912674, + 0.002029003808274865, + -0.013401892967522144, + -0.011006158776581287, + 0.045922696590423584, + -0.040529411286115646, + 0.00991761963814497, + -0.06682797521352768, + 0.05892198532819748, + -0.028617633506655693, + -0.06339140236377716, + 0.0044001322239637375, + -4.362402064591546e-33, + -0.10147389024496078, + 0.039431530982255936, + -0.04599899426102638, + -0.049156975001096725, + -0.02249804697930813, + -0.026725362986326218, + 0.04369090870022774, + -0.0005211823736317456, + -0.030739039182662964, + -0.0065231663174927235, + 0.025333819910883904, + 0.10470504313707352, + 0.021332278847694397, + 0.05556178465485573, + 0.035125020891427994, + -0.1538446545600891, + 0.007054235320538282, + -0.036041803658008575, + 0.05932909622788429, + 0.0337519533932209, + 0.014841740019619465, + 0.030220910906791687, + 0.04296395927667618, + 0.02538421005010605, + -0.006869863253086805, + -0.0031259972602128983, + 0.11390665918588638, + -0.046557214111089706, + 0.00429841224104166, + 0.0428440123796463, + -0.1384042203426361, + 0.04695465788245201, + -0.0574827715754509, + -0.030760489404201508, + -0.07478123158216476, + -0.029447706416249275, + -0.06204935908317566, + -0.03058161959052086, + -0.02502700313925743, + -0.019530421122908592, + -0.02094511315226555, + -0.03218250721693039, + -0.05274674668908119, + -0.02680223248898983, + -0.01955571211874485, + 0.018619371578097343, + 0.007585515268146992, + 0.07155954837799072, + -0.018960801884531975, + -0.05782055854797363, + 0.017819860950112343, + 0.04109930992126465, + -0.03562675416469574, + 0.016705146059393883, + 0.0415029413998127, + 0.05103381723165512, + 0.035572972148656845, + -0.015020242892205715, + -0.03179502114653587, + 0.0891512930393219, + 0.007475084625184536, + 0.042234599590301514, + -0.0542532242834568, + 0.05480321869254112, + -0.10602187365293503, + 0.054763536900281906, + 0.04739809036254883, + -0.03327919542789459, + 0.0082072913646698, + -0.0650608167052269, + -0.08271145075559616, + -0.10922762006521225, + 0.03824940696358681, + 0.05978637561202049, + 0.10005365312099457, + 0.029007570818066597, + -0.034110404551029205, + 0.02545103058218956, + 0.05190473049879074, + -0.003596875350922346, + -0.08473207801580429, + 0.012021052651107311, + 0.018175840377807617, + 0.017920689657330513, + -0.03348258137702942, + 0.04048585146665573, + 0.030843179672956467, + 0.019531769677996635, + -0.00910205114632845, + 0.05721440538764, + 0.007200753781944513, + -0.11394353210926056, + 0.03483451530337334, + 0.029621547088027, + -0.03755185008049011, + 9.393710556981876e-34, + -0.009642334654927254, + -0.001342272269539535, + 0.01180787943303585, + 0.026348579674959183, + -0.011158440262079239, + -0.0019478596514090896, + 0.028490403667092323, + -0.052807874977588654, + -0.029139718040823936, + 0.08380713313817978, + 0.09657946228981018, + 0.00991911068558693, + 0.03502354025840759, + -0.016754556447267532, + 0.017460109665989876, + -0.01642783358693123, + 0.022211389616131783, + -0.008778599090874195, + 3.654864485724829e-05, + -0.09229966253042221, + 0.04083291441202164, + 0.08299239724874496, + -0.04151370748877525, + -0.05360836163163185, + -0.07950830459594727, + 0.03959093242883682, + -0.13340364396572113, + 0.054901909083127975, + 0.05736061558127403, + 0.04770279303193092, + -0.11388063430786133, + 0.0237369854003191, + 0.02305987849831581, + -0.013226242735981941, + -0.020716888830065727, + 0.03368164971470833, + -0.002396275522187352, + -0.012604329735040665, + -0.02715742215514183, + 0.0019254887010902166, + -0.00887741707265377, + -0.028728270903229713, + -0.05864499509334564, + 0.10937061905860901, + -0.004027256276458502, + 0.019197454676032066, + -0.03928225114941597, + -0.018220100551843643, + 0.02466396614909172, + 0.00980517640709877, + 0.05513712763786316, + 0.10625418275594711, + 0.08366440236568451, + -0.02691512741148472, + -0.026599230244755745, + -0.049529410898685455, + -0.05376818776130676, + -0.003328752936795354, + -0.04345694184303284, + 0.11166279017925262, + 0.0407760851085186, + -0.00792837142944336, + 0.03168283402919769, + -0.0018123856279999018, + 0.0357111431658268, + -0.1368872970342636, + 0.00755245191976428, + -0.010281031019985676, + -0.07121222466230392, + 0.03101137839257717, + 0.0662078782916069, + 0.007085337769240141, + -0.007063422352075577, + -0.05238816514611244, + -0.004185437690466642, + 0.00880429521203041, + 0.05528423562645912, + -0.08964741230010986, + 0.0042182342149317265, + -0.029020531103014946, + 0.014597366563975811, + 0.0002752764557953924, + 0.032267484813928604, + 0.11250412464141846, + 0.002768452512100339, + -0.026546282693743706, + 0.017761094495654106, + 0.023686127737164497, + -0.012699384242296219, + 0.018606871366500854, + -0.009665136225521564, + -0.06902427226305008, + -0.04932688549160957, + 0.051991984248161316, + -0.05918996408581734, + -4.18107468647122e-08, + -0.06375139951705933, + -0.0029491656459867954, + 0.032401617616415024, + -0.024946069344878197, + -0.026347795501351357, + 0.020192604511976242, + -0.025759287178516388, + 0.037004951387643814, + 0.043629322201013565, + 0.06278910487890244, + 0.02097572200000286, + -0.07064115256071091, + -0.005097216460853815, + 0.016486743465065956, + -0.0032796994782984257, + 0.07957274466753006, + 0.03776371479034424, + 0.04259084165096283, + -0.005312138702720404, + -0.11992045491933823, + 0.0859612300992012, + -0.0500827394425869, + 0.02553708106279373, + -0.052115052938461304, + -0.07368568331003189, + -0.11394031345844269, + -0.04513951390981674, + 0.022121727466583252, + -0.010451988317072392, + 0.003997989930212498, + -0.13713733851909637, + 0.030619636178016663, + 0.05786789208650589, + 0.05840904265642166, + 0.04732450470328331, + -0.012547117657959461, + -0.027409126982092857, + 0.049672048538923264, + -0.00018399256805423647, + -0.004273589234799147, + -0.011101006530225277, + 0.017240682616829872, + -0.002812016289681196, + 0.034935809671878815, + 0.006601597648113966, + 0.014211715199053288, + -0.06332860141992569, + -0.05397455021739006, + -0.038572490215301514, + -0.06450864672660828, + 0.07963147014379501, + 0.029951799660921097, + 0.03153093531727791, + -0.03852103650569916, + -0.025755248963832855, + 0.13077980279922485, + -0.08185151219367981, + -0.09442253410816193, + 0.01002975832670927, + 0.02509395219385624, + -0.029981056228280067, + -0.025097224861383438, + 0.04001101851463318, + 0.006130080670118332 + ], + "model": "XBN 2.1 Alloy", + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))", + "price": 810, + "store_location": "-0.1278,51.5074" + }, + { + "brand": "ScramBikes", + "condition": "new", + "description": "The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\u2019s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.", + "description_embeddings": [ + -0.006927311420440674, + 0.15900678932666779, + -0.005495064426213503, + 0.06652409583330154, + 0.027506737038493156, + 0.03928178921341896, + 0.0212758406996727, + 0.08142763376235962, + -0.004556896630674601, + 0.04261007905006409, + 0.0852041020989418, + -0.048658017069101334, + 0.04896014556288719, + 0.008289206773042679, + 0.09533551335334778, + 0.042673129588365555, + 0.11990982294082642, + -0.09381455183029175, + -0.001154645229689777, + -0.009989846497774124, + 0.042972203344106674, + 0.055328626185655594, + 0.044754382222890854, + 0.01764347031712532, + 0.04503790661692619, + -0.003830699948593974, + -0.03431522846221924, + 0.03080279380083084, + -0.07722456008195877, + -0.09261710941791534, + 0.04810558632016182, + 0.04917953535914421, + 0.027227703481912613, + -0.04199009761214256, + -0.11837311834096909, + 0.028090154752135277, + 0.08678824454545975, + -0.052036624401807785, + -0.06585966050624847, + -0.04037070646882057, + -0.08040877431631088, + 0.0018185328226536512, + 0.043989114463329315, + -0.011817573569715023, + 0.006730342749506235, + -0.011594205163419247, + 0.008361537009477615, + -0.054372794926166534, + 0.033345889300107956, + -0.03933562710881233, + 0.10115987062454224, + -0.08934503048658371, + 0.04519934952259064, + 0.03582148626446724, + -0.021424520760774612, + -0.07357319444417953, + -0.07426925748586655, + 0.018053170293569565, + 0.04639451205730438, + -0.07499486207962036, + 0.057791586965322495, + -0.05478687211871147, + 0.030340412631630898, + -0.0056327772326767445, + -0.0452428013086319, + -0.044913534075021744, + 0.0526629202067852, + -0.08259481936693192, + -0.05616062134504318, + 0.0005400424124673009, + -0.04933836683630943, + 1.7464293705415912e-05, + -0.007984945550560951, + 0.009467384777963161, + -0.04055757448077202, + -0.0056908270344138145, + 0.036597516387701035, + 0.023873118683695793, + 0.00418807053938508, + 0.04676923528313637, + -0.053795333951711655, + -0.015527212992310524, + -0.080276258289814, + 0.026996882632374763, + 0.1144171804189682, + -0.011178486980497837, + 0.06854862719774246, + 0.022201502695679665, + -0.01913747377693653, + 0.009713435545563698, + 0.01423699501901865, + 0.04772024229168892, + 0.034634001553058624, + 0.0756441131234169, + -0.01204933412373066, + -0.031088251620531082, + -0.02462930977344513, + -0.04386857897043228, + -0.07706759870052338, + -0.002071107504889369, + -0.019828464835882187, + 0.10314348340034485, + 0.07235224545001984, + 0.03167572245001793, + -0.02631053328514099, + -0.036559153348207474, + 0.012040914967656136, + 0.08063311874866486, + 0.008269569836556911, + -0.012434666976332664, + 0.00650354428216815, + -0.05601133033633232, + -0.028117282316088676, + -0.025071244686841965, + -0.01907099038362503, + 0.003760937135666609, + -0.052525606006383896, + 0.07173370569944382, + 0.09372185170650482, + 0.1006019189953804, + 0.023999499157071114, + 0.041847433894872665, + 0.02114962227642536, + 0.05726422369480133, + 0.008218048140406609, + -0.06967302411794662, + 0.01968124881386757, + 1.844028477519647e-33, + -0.031731415539979935, + 0.03648626059293747, + -0.026035945862531662, + -0.026796391233801842, + 0.009511047042906284, + 0.050696976482868195, + -0.03008556365966797, + -0.027142174541950226, + -0.07182826101779938, + -0.02520623430609703, + -0.005983793176710606, + 0.07632830739021301, + 0.09301365166902542, + 0.0744069442152977, + 0.10173527896404266, + -0.08317957073450089, + -0.06219454109668732, + -0.0776224136352539, + 0.013563201762735844, + 0.005839265417307615, + 0.011761599220335484, + -0.0701083242893219, + 0.005647188518196344, + -0.007439272943884134, + -4.4855809392174706e-05, + -0.05095696449279785, + 0.13620494306087494, + 0.017342044040560722, + -0.01352923084050417, + 0.03086051531136036, + -0.0740780308842659, + -0.06721310317516327, + -0.06769067794084549, + 0.01185466069728136, + -0.07234086841344833, + 0.0020215404219925404, + -0.0108563881367445, + 0.007788154762238264, + 0.07912690192461014, + -0.02952556498348713, + -0.010350959375500679, + 0.025074176490306854, + -0.050776757299900055, + 0.00947178341448307, + 0.03259417414665222, + 0.0319671556353569, + 0.0379379540681839, + 0.05242982879281044, + -0.07516643404960632, + 0.020074544474482536, + -0.08712191134691238, + -0.01204316969960928, + -0.006554455496370792, + -0.005875407252460718, + -0.06922540068626404, + 0.041081465780735016, + -0.003257623640820384, + 0.0391659140586853, + -0.03731334209442139, + -0.03579063341021538, + -0.04008869081735611, + 0.05648549646139145, + -0.01142149232327938, + -0.05515728518366814, + -0.06406072527170181, + -0.013155528344213963, + 0.0029902313835918903, + -0.020829875022172928, + -0.13008210062980652, + -0.039412979036569595, + 0.05974568799138069, + -0.06617670506238937, + 0.09968123584985733, + -0.04378099367022514, + 0.008272947743535042, + -0.007245620246976614, + -0.05228377878665924, + -0.05929296463727951, + -0.07816570997238159, + 0.04665905237197876, + -0.01630396395921707, + -0.020880484953522682, + -0.06532212346792221, + -0.003213261254131794, + 0.0416768379509449, + -0.05093088001012802, + -0.0871049091219902, + -0.014571009203791618, + 0.0010796786518767476, + 0.007087667006999254, + 0.05550219491124153, + -0.07753361761569977, + 0.022848013788461685, + 0.02596968039870262, + 0.04259824752807617, + -2.380165022444961e-33, + 0.05820159241557121, + 0.046025633811950684, + 0.10894608497619629, + 0.05244075506925583, + 0.15792003273963928, + 0.024086199700832367, + 0.029237573966383934, + -0.023072607815265656, + -0.07495220005512238, + -0.013094011694192886, + -0.004260784015059471, + -0.053080882877111435, + -0.00895928218960762, + -0.03149857744574547, + 0.13309381902217865, + 0.038643430918455124, + -0.02136029675602913, + -0.030247559770941734, + 0.08822915703058243, + -0.026076463982462883, + 0.050446517765522, + 0.011856893077492714, + -0.06957101821899414, + -0.02773614600300789, + 0.04625667631626129, + 0.008786994963884354, + -0.07701855897903442, + 0.001076446962542832, + -0.004858669359236956, + 0.0067254831083118916, + -0.02791707031428814, + -0.019495468586683273, + 0.0703381896018982, + 0.030558260157704353, + -0.05546342208981514, + 0.0420801043510437, + -0.03730127215385437, + -0.01322201732546091, + 0.038336288183927536, + 0.06427455693483353, + 0.028238974511623383, + -0.06713984161615372, + 0.03150900453329086, + 0.024600861594080925, + 0.016221575438976288, + 0.025838203728199005, + -0.02313261851668358, + 0.0557485967874527, + 0.015211678110063076, + 0.029895616695284843, + 0.06008606031537056, + 0.04473312199115753, + -0.050083715468645096, + -0.017571061849594116, + -0.014091495424509048, + -0.06641074270009995, + -0.008071008138358593, + 0.03312089666724205, + -0.09482058882713318, + -0.08763624727725983, + 0.030528157949447632, + 0.013274747878313065, + 0.06158939003944397, + 0.0748405009508133, + -0.12128522247076035, + -0.12076683342456818, + 0.0464591421186924, + 0.0045278542675077915, + -0.002574144396930933, + -0.028266169130802155, + -0.04953430965542793, + -0.007604515179991722, + -0.01084962673485279, + -0.06370151042938232, + 0.00812828354537487, + -0.027805447578430176, + 0.044624149799346924, + 0.016610005870461464, + -0.027103105559945107, + -0.046117331832647324, + -0.002852817066013813, + 0.05479831248521805, + 0.05342277139425278, + -0.03752776235342026, + -0.0053502353839576244, + -0.018950853496789932, + -0.07712738960981369, + -0.13029317557811737, + -0.013171681202948093, + 0.02069253847002983, + 0.029317859560251236, + 0.018668048083782196, + -0.061371881514787674, + 0.05376929044723511, + 0.01483837515115738, + -3.964297690117746e-08, + 0.03559036925435066, + -0.005289694294333458, + -0.013635152950882912, + -0.020672710612416267, + 0.039649732410907745, + -0.06206038221716881, + 0.04933065176010132, + 0.010378899984061718, + 0.020493512973189354, + 0.027486076578497887, + 0.07419533282518387, + -0.03473059833049774, + 0.05251400172710419, + 0.013588557951152325, + 0.01814397983253002, + -0.024619124829769135, + 0.11751414835453033, + 0.05483951419591904, + 0.022471528500318527, + 0.0301313828676939, + 0.018649602308869362, + -0.06219587102532387, + 0.04238469526171684, + -0.053293377161026, + 0.002427657600492239, + -0.0019911346025764942, + 0.004944113548845053, + -0.061433035880327225, + -0.013191037811338902, + -0.030179856345057487, + -0.051340680569410324, + 0.022094713523983955, + -0.035341646522283554, + -0.007219062652438879, + -0.06182244420051575, + -0.06059279292821884, + -0.041057273745536804, + 0.002089729532599449, + -0.043397895991802216, + 0.07170896232128143, + 0.036471735686063766, + -0.015439609996974468, + -0.01848314143717289, + 0.0056609525345265865, + -0.0012752920156344771, + -0.056571539491415024, + -0.01979043148458004, + -0.12011151015758514, + -0.02223217859864235, + 0.029830070212483406, + 0.027551136910915375, + -0.0010704555315896869, + 0.000702365068718791, + -0.005173679441213608, + -0.019360698759555817, + 0.17009279131889343, + -0.04601005092263222, + -0.04016156122088432, + -0.01600208878517151, + 0.04198170080780983, + 0.02032368630170822, + -0.06263051927089691, + -0.048464443534612656, + 0.009351130574941635 + ], + "model": "WattBike", + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))", + "price": 2300, + "store_location": "2.3522,48.8566" + }, + { + "brand": "Peaknetic", + "condition": "new", + "description": "If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "description_embeddings": [ + 0.0022135202307254076, + 0.0681997612118721, + 0.0064607178792357445, + 0.007070810534060001, + -0.054231829941272736, + 0.008059866726398468, + 0.040072083473205566, + 0.0956423208117485, + 0.049143481999635696, + 0.0379914790391922, + -0.020757146179676056, + 0.06460786610841751, + 0.07982552796602249, + -0.0321788415312767, + -0.006194670218974352, + 0.0375053696334362, + 0.1270381063222885, + -0.02341461181640625, + -0.007247396744787693, + 0.0682845488190651, + -0.026470346376299858, + 0.005658577661961317, + 0.04653697833418846, + 0.03987714648246765, + -0.12344618886709213, + 0.03188862279057503, + -0.02708076685667038, + 0.06964577734470367, + 0.02201199159026146, + -0.02890665829181671, + -0.040280576795339584, + 0.06318672746419907, + 0.07309666275978088, + -0.08932560682296753, + -0.11648543179035187, + -0.029937371611595154, + 0.10487373173236847, + 0.041655637323856354, + -0.022871389985084534, + -0.04677774757146835, + 0.02746077999472618, + 0.01847464218735695, + 0.08173345029354095, + 0.06561841815710068, + 0.05970228090882301, + -0.024480478838086128, + 0.06907600909471512, + -0.03947169706225395, + 0.024855393916368484, + -0.013199429027736187, + 0.09996063262224197, + 0.0028578736819326878, + 0.08506831526756287, + 0.014810853637754917, + -0.03929787874221802, + 0.0018542942125350237, + -0.14215077459812164, + 0.06970464438199997, + 0.0265016071498394, + -0.06698428839445114, + 0.07732488960027695, + -0.04583248123526573, + 0.028696633875370026, + 0.013155206106603146, + 0.02578958310186863, + -0.029291296377778053, + -0.00031816380214877427, + -0.125708669424057, + -0.042060736566782, + -0.05381627008318901, + -0.0538906455039978, + 0.0014778936747461557, + -0.09889215975999832, + -0.0429794080555439, + -0.013458915054798126, + -0.04558553919196129, + 0.09005370736122131, + -0.04213777929544449, + -0.1349189579486847, + 0.055852413177490234, + 0.006197084207087755, + 0.016338912770152092, + 0.032463233917951584, + 0.02548987604677677, + 0.030681701377034187, + -0.0703219398856163, + 0.05395807698369026, + -0.009628057479858398, + -0.031475286930799484, + -0.027579376474022865, + 0.04098684713244438, + 0.03901920095086098, + -0.01192146260291338, + -0.04101356491446495, + -0.08753280341625214, + 0.0705665871500969, + 0.03578994423151016, + 0.032069701701402664, + -0.03211764618754387, + 0.09590566158294678, + 0.0611797571182251, + 0.004700345452874899, + 0.10438291728496552, + 0.07409676164388657, + -0.0090691689401865, + 0.011407091282308102, + 0.05370165407657623, + -0.020438825711607933, + -0.04398776963353157, + 0.032895661890506744, + -0.0012730252929031849, + 0.018297234550118446, + -0.04333885386586189, + 0.06208101287484169, + -0.08955910801887512, + -0.04672509804368019, + -0.06376977264881134, + 0.022537674754858017, + 0.02881341427564621, + -0.0030273371376097202, + -0.06542699784040451, + 0.007231372408568859, + 0.01855703443288803, + 0.012365416623651981, + 0.018434280529618263, + -0.04855069890618324, + -0.08907819539308548, + -2.615521077916409e-34, + -0.046131156384944916, + -0.028526470065116882, + 0.01342072058469057, + -0.010418130084872246, + -0.019669201225042343, + -0.07430055737495422, + -0.006000629626214504, + -0.03320513665676117, + -0.05303066596388817, + 0.07680810242891312, + -0.0061429338529706, + 0.06154831871390343, + 0.09432042390108109, + 0.0038354273419827223, + 0.14506474137306213, + -0.07032876461744308, + -0.07443196326494217, + -0.04439792409539223, + -0.005104243289679289, + -3.3934433304239064e-05, + -0.08257872611284256, + -0.056131236255168915, + 0.04174238070845604, + 0.0017745267832651734, + -0.002772972686216235, + 0.006998051423579454, + 0.09371285885572433, + -0.013138788752257824, + -0.042773280292749405, + 0.00896062795072794, + -0.09441300481557846, + -0.007952050305902958, + -0.015586148016154766, + -0.017506571486592293, + 0.01347337942570448, + 0.027937229722738266, + -0.11657726019620895, + -0.03324989974498749, + -0.018427638337016106, + -0.04819897934794426, + -0.04077771306037903, + -0.0001697312545729801, + -0.05697356536984444, + 0.07191669940948486, + 0.017228955402970314, + 0.03711475431919098, + 0.042057864367961884, + 0.004786928184330463, + -0.06956200301647186, + 0.06036132574081421, + -0.0016305814497172832, + 0.02468821406364441, + -0.013794313184916973, + -0.025994934141635895, + -0.07489914447069168, + 0.01067660003900528, + -0.010298662818968296, + -0.022154971957206726, + -0.09746527671813965, + 0.06335768103599548, + -0.022728655487298965, + -0.0005628599901683629, + -0.04313022270798683, + -0.04123309254646301, + -0.08378659188747406, + 0.020546691492199898, + 0.01680913008749485, + -0.010155763477087021, + 0.008547178469598293, + -0.0211178008466959, + 0.023050235584378242, + 0.06479462236166, + 0.061528537422418594, + 0.022156352177262306, + 0.019619867205619812, + 0.07456795871257782, + 0.049971628934144974, + -0.07878398895263672, + 0.04972408711910248, + -0.015442566946148872, + 0.044758349657058716, + -0.037393294274806976, + -0.03831558674573898, + -0.016534404829144478, + -0.05692937225103378, + 0.02438316121697426, + -0.008604591712355614, + -0.06003899499773979, + 0.04884570837020874, + -0.05407913774251938, + -0.004864877089858055, + -0.039421387016773224, + -0.0335439033806324, + 0.05149518698453903, + -0.00761334178969264, + -4.919814414418271e-34, + 0.0605306513607502, + 0.013155259191989899, + 0.0022042335476726294, + -0.016238724812865257, + 0.017820321023464203, + -0.029504502192139626, + 0.1179562360048294, + -0.08494631201028824, + -0.05847567319869995, + -0.03412671759724617, + 0.01584434323012829, + 0.0205944012850523, + 0.011703073978424072, + 0.024820921942591667, + 0.07683313637971878, + 0.016703670844435692, + -0.0465223602950573, + -0.08929521590471268, + 0.013778958469629288, + -0.06448621302843094, + 0.047237128019332886, + 0.11140323430299759, + -0.036479681730270386, + -0.055252984166145325, + -0.08155377954244614, + -0.0011454259511083364, + -0.07414430379867554, + 0.04839516803622246, + -0.036127883940935135, + 0.04249665141105652, + -0.05564387887716293, + 0.008439254947006702, + 0.056384216994047165, + 0.0037136792670935392, + -0.06267311424016953, + 0.06599505990743637, + -0.0944608598947525, + -0.01900491677224636, + 0.0017078104428946972, + 0.018181079998612404, + 0.0072936019860208035, + 0.03901578113436699, + 0.022009696811437607, + -0.02042953297495842, + 0.04422936588525772, + 0.099826380610466, + 0.0022175773046910763, + 0.05569764971733093, + 0.005229232832789421, + -0.06281892955303192, + 0.06203164905309677, + 0.05493532121181488, + 0.06598878651857376, + 0.06272768974304199, + 0.05623844638466835, + -0.053184594959020615, + 0.012609804049134254, + -0.009191329590976238, + -0.07956400513648987, + -0.005095178727060556, + -0.06295286118984222, + 0.05799472704529762, + 0.004298996180295944, + -0.0245245061814785, + -0.010027579963207245, + -0.02338206022977829, + -0.03576310724020004, + -0.0735088586807251, + -0.10477573424577713, + 0.010318689979612827, + -0.031293049454689026, + -0.06913845986127853, + 0.07762496173381805, + -0.029345618560910225, + 0.03347797319293022, + 0.014305013231933117, + 0.06321006268262863, + -0.09872464835643768, + -0.040937263518571854, + 0.028350593522191048, + -0.08784928917884827, + 0.005375882610678673, + 0.059733789414167404, + 0.027322115376591682, + -0.04657347500324249, + 0.02783248759806156, + -0.12815773487091064, + 0.004414730705320835, + -0.009058866649866104, + 0.06062375009059906, + 0.04072198644280434, + 0.05082780495285988, + -0.036888547241687775, + 0.07301440089941025, + 0.022250277921557426, + -4.836826761334123e-08, + -0.02333931438624859, + -0.0006597689352929592, + -0.011001646518707275, + 0.01690472476184368, + -0.0037487675435841084, + 0.039681874215602875, + -0.012263616546988487, + 0.010128140449523926, + 0.03165998309850693, + 0.0028211181052029133, + 0.009292887523770332, + -0.05068610981106758, + -0.05350435897707939, + 0.0027450143825262785, + -0.02869512513279915, + 0.18523184955120087, + 0.010015024803578854, + 0.04042758792638779, + 0.010014397092163563, + -0.07638011872768402, + -0.04126984626054764, + -0.06051325798034668, + 0.04609273001551628, + -0.00863336119800806, + -0.010159372352063656, + -0.0686831921339035, + -0.022274712100625038, + 0.054596077650785446, + -0.004217579495161772, + 0.005010589957237244, + -0.0023027928546071053, + 0.054350487887859344, + 0.051168326288461685, + -0.0005977987311780453, + 0.0008231411338783801, + -0.005349422339349985, + -0.04023698344826698, + 0.01875622756779194, + 0.051792532205581665, + 0.0725129023194313, + 0.003990984987467527, + -0.0412585511803627, + -0.030975697562098503, + 0.02229507826268673, + -0.023536084219813347, + 0.004254089202731848, + -0.047596003860235214, + 0.054139409214258194, + 0.024750487878918648, + 0.00830126740038395, + 0.04204926639795303, + -0.05580601468682289, + 0.014396552927792072, + 0.0785103291273117, + -0.01755904220044613, + 0.09679318964481354, + -0.01722336746752262, + -0.03680667281150818, + -0.021823544055223465, + 0.044612541794776917, + -0.0008680447936058044, + 0.027066197246313095, + -0.008826435543596745, + -0.034606464207172394 + ], + "model": "Secto", + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))", + "price": 430, + "store_location": "13.4050,52.5200" + }, + { + "brand": "nHill", + "condition": "new", + "description": "This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.", + "description_embeddings": [ + -0.024333441630005836, + 0.029308300465345383, + -0.012847754172980785, + -0.013123984448611736, + -0.039905961602926254, + -0.04596070945262909, + -0.01287133153527975, + 0.03984961286187172, + -0.043438445776700974, + 0.012349214404821396, + -0.021342391148209572, + -0.027458613738417625, + -0.004793129395693541, + 0.016479987651109695, + 0.02052542380988598, + -0.009542089886963367, + 0.10066469013690948, + 0.017397526651620865, + 0.07893014699220657, + -0.0765308141708374, + -0.007650941610336304, + 9.538845188217238e-05, + 0.03314477205276489, + 0.026631362736225128, + -0.01621824875473976, + 0.03593063727021217, + -0.014871303923428059, + 0.05259871482849121, + 0.027785824611783028, + -0.044416770339012146, + -0.026691904291510582, + -0.01390022411942482, + -0.0573929138481617, + -0.08761026710271835, + 0.012642575427889824, + 0.04252056032419205, + 0.04015251249074936, + -0.0052475095726549625, + -0.08608037978410721, + 0.0028531427960842848, + -0.003371630096808076, + -0.02032443881034851, + -0.029511747881770134, + -0.043690912425518036, + 0.042122989892959595, + -0.057133886963129044, + 0.019293654710054398, + -0.019127611070871353, + 0.036713697016239166, + -0.00833116751164198, + 0.009040397591888905, + -0.1087319627404213, + 0.09127230197191238, + -0.048213958740234375, + -0.023604029789566994, + 0.0008899428648874164, + -0.093184694647789, + -0.03436879441142082, + 0.00807907897979021, + -0.0520450733602047, + 0.02262270636856556, + -0.03259866312146187, + -0.0391012541949749, + 0.001295328140258789, + 0.06636986881494522, + -0.04248524457216263, + -0.07093628495931625, + -0.06715728342533112, + 0.007415436673909426, + -0.06185786426067352, + 0.036563266068696976, + 0.030749274417757988, + 0.007705106865614653, + 0.0023627770133316517, + -0.026215652003884315, + -0.020864665508270264, + 0.05270038917660713, + 0.021477004513144493, + 0.0068344962783157825, + 0.024355463683605194, + -0.03746088594198227, + 0.031043346971273422, + 0.09879995882511139, + -0.050035204738378525, + 0.08045479655265808, + -0.04274336248636246, + 0.02994127944111824, + -0.03783947601914406, + 0.039749279618263245, + 0.046901557594537735, + 0.08667739480733871, + 0.02831847593188286, + -0.008148318156599998, + 0.007401767186820507, + -0.10989774018526077, + -0.014092149212956429, + 9.036048140842468e-06, + 0.054770372807979584, + -0.012413500808179379, + 0.01861056312918663, + 0.060349978506565094, + 0.06291640549898148, + 0.007055714726448059, + -0.021361181512475014, + 0.03852728381752968, + -0.018908292055130005, + 0.047424111515283585, + 0.054501011967659, + 0.017648596316576004, + 0.11429999768733978, + -0.041358742862939835, + -0.03363256901502609, + 0.0030338421929627657, + 0.030056871473789215, + 0.008060693740844727, + -0.0688784271478653, + -0.09984277933835983, + 0.030267179012298584, + -0.028509166091680527, + 0.02268315851688385, + -0.07580948621034622, + -0.0405895970761776, + 0.03384138271212578, + 0.07081738114356995, + 0.019772004336118698, + -0.06172173097729683, + 0.008248022757470608, + 1.9840379362262432e-33, + 0.037930894643068314, + 0.04577890411019325, + -0.03732670471072197, + -0.032416991889476776, + 0.014588817022740841, + -0.06602118909358978, + -0.013814577832818031, + -0.10539791733026505, + -0.12850415706634521, + -0.005076196976006031, + -0.07302963733673096, + 0.07235066592693329, + -0.04646436870098114, + 0.059371400624513626, + 0.09111672639846802, + -0.10133209824562073, + -0.05483068525791168, + -0.053797587752342224, + -0.03709062561392784, + 0.08241501450538635, + 0.03958267718553543, + 0.01433452870696783, + 0.05912068858742714, + -0.0029660870786756277, + 0.01806815154850483, + -0.0680701732635498, + 0.013625666499137878, + 0.015582672320306301, + -0.02142208069562912, + 0.06357447057962418, + -0.12094264477491379, + -0.09666852653026581, + -0.03333089128136635, + -0.06621623784303665, + -0.03821765258908272, + -0.010776739567518234, + -0.08198492974042892, + -0.03277386352419853, + 0.014794036746025085, + 0.013517632149159908, + -0.0034003634937107563, + 0.005664682947099209, + -0.022207774221897125, + 0.015086682513356209, + -0.06799489259719849, + 0.08191754668951035, + 0.15600024163722992, + 0.07579171657562256, + -0.05843832343816757, + -0.008412746712565422, + -0.07857701182365417, + -0.0038521387614309788, + -0.02901686355471611, + 0.0006679021753370762, + -0.0697081908583641, + -0.05330892279744148, + 0.053166620433330536, + 0.004783661104738712, + 0.013741954229772091, + 0.07203977555036545, + -0.015861764550209045, + 0.033332549035549164, + -0.022547632455825806, + -0.011850795708596706, + -0.09526507556438446, + -0.016078950837254524, + -0.004922114312648773, + 0.004731250926852226, + -0.006536174099892378, + 0.004852978512644768, + 0.01268527377396822, + 0.05300697684288025, + 0.11899229139089584, + 0.025274425745010376, + 0.11763036996126175, + -0.08781222999095917, + -0.016429787501692772, + -0.02661072462797165, + -0.01769474893808365, + 0.05961911380290985, + 0.005877051502466202, + -0.03780582174658775, + -0.028037618845701218, + 0.023397648707032204, + -0.006716609466820955, + 0.015020519495010376, + 0.040884219110012054, + -0.06197969987988472, + -0.029691174626350403, + -0.004976964555680752, + -0.03422437235713005, + 0.018984060734510422, + -0.00813105795532465, + -0.020747167989611626, + 0.0281930360943079, + -3.2326105667872445e-33, + 0.12847235798835754, + 0.026267321780323982, + 0.10983140766620636, + 0.011159190908074379, + -0.044980239123106, + 0.017553674057126045, + -0.004794386215507984, + -0.015802551060914993, + -0.01226617582142353, + -0.019019361585378647, + 0.011876302771270275, + -0.023136194795370102, + 0.03209578990936279, + 0.07442695647478104, + 0.05641649663448334, + -0.05300089344382286, + -0.03638714924454689, + -0.014843880198895931, + 0.036886364221572876, + -0.1257546991109848, + -0.008151554502546787, + 0.07542898505926132, + -0.11252967268228531, + -0.09049033373594284, + -0.0030374047346413136, + 0.04011000320315361, + -0.1538117229938507, + 0.07717141509056091, + -0.04436258599162102, + 0.07290291786193848, + -0.0072043901309370995, + 0.03950807452201843, + 0.002569766016677022, + -0.006248284596949816, + 0.001176450401544571, + 0.12515375018119812, + 0.02005600742995739, + -0.00013864059292245656, + 0.024221621453762054, + 0.05465780198574066, + 0.09940708428621292, + -0.053944025188684464, + 0.09602796286344528, + 0.00014552564243786037, + 0.026602625846862793, + 0.016811460256576538, + 0.0015170868718996644, + 0.08731727302074432, + 0.005771235562860966, + -0.009904555976390839, + 0.06958170980215073, + 0.09265917539596558, + 0.005830116104334593, + 0.10210693627595901, + 0.0011325017549097538, + -0.006749417632818222, + 0.01503710262477398, + -0.03714510798454285, + -0.0939728170633316, + -0.03357759863138199, + -0.050196800380945206, + -0.006316252052783966, + -0.06414957344532013, + 0.007235861383378506, + -0.01670873910188675, + -0.07423098385334015, + 0.018221961334347725, + -0.05320512503385544, + -0.010248126462101936, + 0.013013344258069992, + -0.12916545569896698, + -0.04244375228881836, + 0.08774643391370773, + -0.03917357325553894, + -0.027767449617385864, + 0.02099072001874447, + 0.0595061257481575, + 0.01945589855313301, + 0.03815269470214844, + -0.028747402131557465, + -0.025533201172947884, + -0.038349006325006485, + -0.014486055821180344, + -0.013576257973909378, + 0.051731329411268234, + 0.06515145301818848, + -0.011720783077180386, + -0.1163521260023117, + -0.03931708261370659, + 0.03394515439867973, + 0.09380073100328445, + 0.00931628793478012, + -0.01171857863664627, + 0.04104544222354889, + -0.008916886523365974, + -4.140364140425845e-08, + 0.061763226985931396, + 0.02101300284266472, + -0.019075985997915268, + -0.002017116406932473, + 0.015830116346478462, + -0.009857675060629845, + -0.018932191655039787, + -0.0007055550813674927, + 0.013249439187347889, + 0.0642324835062027, + 0.12860508263111115, + 0.03213700279593468, + -0.0670522004365921, + 0.041800037026405334, + -0.0426088348031044, + 0.0437115803360939, + 0.021514814347028732, + 0.12901803851127625, + 0.015236952342092991, + -0.02232329733669758, + -0.05031055584549904, + -0.052778035402297974, + 0.000233030179515481, + -0.022497626021504402, + -0.014370465651154518, + 0.017518600448966026, + 0.014312495477497578, + 0.010902844369411469, + 0.0027771666646003723, + -0.04195915535092354, + -0.06504479050636292, + 0.038553908467292786, + -0.01374481339007616, + 0.03843652829527855, + 0.09058628976345062, + -0.0230315700173378, + -0.11761228740215302, + 0.06487669050693512, + -0.005885662976652384, + 0.06288695335388184, + 0.05191958323121071, + 0.011840583756566048, + -0.027309859171509743, + 0.040654800832271576, + -0.005980184301733971, + 0.03252403438091278, + -0.043744225054979324, + 0.006068602204322815, + -0.00032751375692896545, + -0.022902367636561394, + -0.050694938749074936, + 0.026743048802018166, + 0.06005466729402542, + 0.02092430181801319, + 0.02072584442794323, + 0.08342021703720093, + -0.05768749862909317, + -0.08585236966609955, + -0.03160274401307106, + 0.05222279205918312, + 0.029474787414073944, + -0.08763033896684647, + -0.01591801643371582, + -0.07798203080892563 + ], + "model": "Summit", + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))", + "price": 1200, + "store_location": "2.1734, 41.3851" + }, + { + "brand": "BikeShind", + "condition": "refurbished", + "description": "An artsy, retro-inspired bicycle that\u2019s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\u2019t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.", + "description_embeddings": [ + -0.005154624115675688, + 0.10099548101425171, + 0.04890008643269539, + 0.0625317171216011, + -0.0363173745572567, + 0.025850096717476845, + 0.032137978821992874, + -0.03442877531051636, + -0.10096120089292526, + -0.007441149093210697, + 0.002616145182400942, + 0.002316742204129696, + 0.042584337294101715, + 0.026208963245153427, + 0.018415318801999092, + 0.050033390522003174, + 0.14831797778606415, + -0.006160213612020016, + 0.05086936056613922, + 0.0131875304505229, + -0.05973455682396889, + 0.045886117964982986, + -0.025052331387996674, + 0.04576171189546585, + -0.006168896332383156, + 0.03315199911594391, + -0.07763667404651642, + 0.05837876722216606, + -0.03229084238409996, + -0.021293187513947487, + -0.008595496416091919, + 0.06880632787942886, + -0.04528025537729263, + -0.020664094015955925, + -0.029125794768333435, + 0.0445701889693737, + 0.023799002170562744, + -0.013527574017643929, + -0.01646343804895878, + 0.027023920789361, + 0.0014843698590993881, + 0.014952401630580425, + -0.010172702372074127, + 0.07603776454925537, + 0.017975280061364174, + -0.08911248296499252, + 0.055482957512140274, + -0.027819648385047913, + 0.019395964220166206, + 0.05424710363149643, + 0.06634850054979324, + -0.05045575276017189, + -0.0006240577204152942, + -0.06339468061923981, + -0.023632608354091644, + -0.060364823788404465, + -0.11193189769983292, + 0.046562716364860535, + -0.010353066958487034, + -0.043506212532520294, + 0.051063187420368195, + 0.003231980837881565, + 0.027102531865239143, + 0.002380193443968892, + -0.0056939758360385895, + -0.026826998218894005, + -0.014482468366622925, + -0.0185822993516922, + 0.013927196152508259, + -0.10704471915960312, + 0.0673733651638031, + -0.0010599792003631592, + -0.07455477863550186, + -0.014452059753239155, + -0.02294282428920269, + -0.010201872326433659, + 0.023037923499941826, + -0.05595972761511803, + -0.09120096266269684, + -0.029177049174904823, + -0.02343042939901352, + -0.029726101085543633, + 0.07366959005594254, + -0.046458736062049866, + 0.006261167582124472, + -0.013148028403520584, + -0.0014217026764526963, + 0.017440352588891983, + -0.04511420056223869, + 0.008813070133328438, + -0.022654451429843903, + -0.014097515493631363, + -0.020799456164240837, + -0.05456947907805443, + -0.07338607311248779, + 0.017073828727006912, + -0.04925030097365379, + -0.024428002536296844, + -0.00979585014283657, + 0.025696750730276108, + 0.07033320516347885, + 0.0389082096517086, + 0.12711860239505768, + 0.07472020387649536, + -0.019501805305480957, + 0.01663217693567276, + 0.027799105271697044, + 0.039703886955976486, + -0.029871169477701187, + 0.005997804459184408, + -0.0015701024094596505, + 0.0411832258105278, + 8.85713889147155e-05, + -0.029442811384797096, + 0.009789851494133472, + -0.04723270237445831, + -0.09891993552446365, + 0.05102938041090965, + -0.007144609931856394, + 0.01668076030910015, + 0.004902719985693693, + 0.02218448370695114, + 0.03827798366546631, + 0.001542922342196107, + 0.004610520787537098, + -0.12595036625862122, + 0.07361572980880737, + -1.521196586481568e-33, + -0.01484542153775692, + 0.024973904713988304, + 0.006633534096181393, + 0.01254032738506794, + 0.06355929374694824, + -0.04809075966477394, + 0.02005922608077526, + -0.06251886487007141, + -0.06941430270671844, + 0.02304103970527649, + -0.020975833758711815, + 0.08137834072113037, + 0.049319952726364136, + 0.11945393681526184, + 0.17472721636295319, + -0.03430051729083061, + -0.07276412844657898, + -0.11828659474849701, + 0.04918891564011574, + -0.07279565185308456, + -0.06228293105959892, + -0.03494764119386673, + 0.004517378751188517, + -0.0431801863014698, + -0.07457974553108215, + -0.0003888327337335795, + 0.05853814631700516, + 0.05362739413976669, + 0.01866878569126129, + 0.004751134198158979, + -0.07936650514602661, + 0.033309247344732285, + -0.01660560630261898, + -0.08851712942123413, + 0.042956508696079254, + 0.03460550308227539, + -0.054352447390556335, + -0.016094308346509933, + 0.019760286435484886, + -0.03591147065162659, + -0.04307156056165695, + -0.04029138758778572, + -0.09315019100904465, + 0.055713504552841187, + 0.003642909461632371, + 0.0391569547355175, + 0.1300928145647049, + 0.026774607598781586, + -0.054334208369255066, + -0.060861144214868546, + -0.020662454888224602, + -0.034525640308856964, + 0.004193050786852837, + 0.02453654631972313, + -0.09028972685337067, + 0.004444751888513565, + 0.06320545822381973, + 0.01895289309322834, + -0.04660886526107788, + 0.08611723780632019, + 0.05705415457487106, + 0.030612647533416748, + 0.019753821194171906, + -0.061483170837163925, + -0.025262145325541496, + 0.054222241044044495, + 0.017605014145374298, + -0.013110420666635036, + 0.04744894057512283, + -0.004785404074937105, + 0.04680679365992546, + 0.001831064117141068, + 0.07773134112358093, + 0.008347200229763985, + 0.08674833923578262, + 0.024294976145029068, + -0.03695613518357277, + -0.0440739244222641, + 0.00895663257688284, + -0.0060102143324911594, + -0.0361272394657135, + -0.08447428047657013, + -0.08971717208623886, + 0.03131894767284393, + 0.0721370130777359, + 0.031180810183286667, + 0.039390258491039276, + -0.09044987708330154, + -0.007888346910476685, + 0.008131768554449081, + -0.03624662756919861, + -0.033649034798145294, + 0.031620997935533524, + 0.05870470404624939, + 0.018141048029065132, + -5.7662626416839565e-34, + 0.09117024391889572, + -0.023953478783369064, + 0.080879345536232, + 0.0035459804348647594, + -0.013222851790487766, + -0.025237491354346275, + 0.019038159400224686, + -0.06588941067457199, + -0.11689981818199158, + 0.0007165187271311879, + -0.0933104082942009, + 0.014618181623518467, + 0.12644915282726288, + 0.0027253602165728807, + 0.15038658678531647, + 0.0053163510747253895, + -0.005645217839628458, + 0.009321562945842743, + 0.002719732467085123, + -0.01651378907263279, + 0.0063886744901537895, + 0.05738385394215584, + -0.14213474094867706, + -0.0669025406241417, + -0.05766160413622856, + 0.0031909833196550608, + -0.13478563725948334, + -0.03741385415196419, + 0.041654907166957855, + 0.04030514881014824, + -0.05102578178048134, + 0.0028968513943254948, + 0.0738285705447197, + -0.03643025830388069, + -0.015445208176970482, + 0.06674294173717499, + 0.029759766533970833, + -0.024196317419409752, + -0.009355752728879452, + 0.023269686847925186, + 0.01623220555484295, + -0.08251868188381195, + 0.020170293748378754, + 0.0749305933713913, + 0.08206077665090561, + 0.012237507849931717, + -0.026570556685328484, + 0.016253553330898285, + -0.003425935748964548, + 0.005059909541159868, + 0.12109038233757019, + 0.08107315003871918, + -0.04097432270646095, + 0.0009462210582569242, + 0.058092083781957626, + -0.03609737753868103, + -0.01580999232828617, + -0.0342475026845932, + 0.012185491621494293, + -0.058348096907138824, + -0.05934947729110718, + 0.026715252548456192, + -0.031246516853570938, + 0.012632215395569801, + -0.01100403256714344, + -0.05962579324841499, + -0.000801989808678627, + -0.06919746845960617, + -0.1299775391817093, + -0.03721063956618309, + -0.03989635780453682, + 0.04918158799409866, + -0.0022671804763376713, + -0.04244564101099968, + -0.02631748840212822, + -0.04100838303565979, + 0.08634430915117264, + 0.05031242221593857, + 0.06688959896564484, + 0.024407675489783287, + 0.0018788984743878245, + -0.026026425883173943, + 0.04438408091664314, + 0.02198263816535473, + -0.014974343590438366, + -0.026490231975913048, + -0.09914876520633698, + -0.09834708273410797, + 0.017346272245049477, + 0.00089951854897663, + 0.05497178062796593, + 0.06180766969919205, + -0.050425756722688675, + 0.013637324795126915, + -0.03599657490849495, + -4.404103037813911e-08, + -0.0024933917447924614, + 0.049585312604904175, + -0.020710783079266548, + -0.014283453114330769, + -0.0064896950498223305, + 0.04579087719321251, + 0.02242390066385269, + 0.02515084482729435, + -0.031156959012150764, + 0.006718308199197054, + 0.061417631804943085, + -0.009935885667800903, + -0.021248064935207367, + 0.011152776889503002, + -0.059073783457279205, + 0.05840323865413666, + 0.06523993611335754, + 0.03690555691719055, + 0.03174441307783127, + 0.02336188219487667, + 0.003073832020163536, + -0.017117813229560852, + 0.02877660095691681, + 0.0017827276606112719, + -0.07995487004518509, + -0.032443612813949585, + -0.01474942360073328, + -0.016080526635050774, + 0.0771399736404419, + 0.017354682087898254, + -0.02578366920351982, + 0.004991421941667795, + 0.006590475793927908, + -0.032372940331697464, + 0.038421064615249634, + -0.1204696074128151, + -0.05457846075296402, + 0.036750562489032745, + 0.013932188041508198, + 0.0031626380514353514, + 0.009978784248232841, + -0.0138266421854496, + 0.006769631989300251, + 0.059896957129240036, + 0.00822784099727869, + -0.05025117099285126, + -0.10305225104093552, + -0.07187453657388687, + 0.00836241990327835, + 0.049537766724824905, + 0.007570290472358465, + -0.08729138970375061, + -0.020592620596289635, + 0.04918212443590164, + 0.054685790091753006, + 0.04091939702630043, + -0.05420788377523422, + -0.04473182559013367, + -0.11178043484687805, + 0.09808126091957092, + -0.015394269488751888, + -0.0039725536480546, + 0.03283742815256119, + -0.05677533522248268 + ], + "model": "ThrillCycle", + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, 12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "price": 815, + "store_location": "12.4964,41.9028" + } +] \ No newline at end of file diff --git a/doctests/dt-bitfield.js b/doctests/dt-bitfield.js new file mode 100644 index 00000000000..9685372de41 --- /dev/null +++ b/doctests/dt-bitfield.js @@ -0,0 +1,76 @@ +// EXAMPLE: bitfield_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START bf +let res1 = await client.bitField("bike:1:stats", [{ + operation: 'SET', + encoding: 'u32', + offset: '#0', + value: 1000 +}]); +console.log(res1); // >>> [0] + +let res2 = await client.bitField('bike:1:stats', [ + { + operation: 'INCRBY', + encoding: 'u32', + offset: '#0', + increment: -50 + }, + { + operation: 'INCRBY', + encoding: 'u32', + offset: '#1', + increment: 1 + } +]); +console.log(res2); // >>> [950, 1] + +let res3 = await client.bitField('bike:1:stats', [ + { + operation: 'INCRBY', + encoding: 'u32', + offset: '#0', + increment: 500 + }, + { + operation: 'INCRBY', + encoding: 'u32', + offset: '#1', + increment: 1 + } +]); +console.log(res3); // >>> [1450, 2] + +let res4 = await client.bitField('bike:1:stats', [ + { + operation: 'GET', + encoding: 'u32', + offset: '#0' + }, + { + operation: 'GET', + encoding: 'u32', + offset: '#1' + } +]); +console.log(res4); // >>> [1450, 2] +// STEP_END + +// REMOVE_START +assert.deepEqual(res1, [0]) +assert.deepEqual(res2, [950, 1]) +assert.deepEqual(res3, [1450, 2]) +assert.deepEqual(res4, [1450, 2]) +await client.close(); +// REMOVE_END diff --git a/doctests/dt-bitmap.js b/doctests/dt-bitmap.js new file mode 100644 index 00000000000..845fd2a721c --- /dev/null +++ b/doctests/dt-bitmap.js @@ -0,0 +1,39 @@ +// EXAMPLE: bitmap_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START ping +const res1 = await client.setBit("pings:2024-01-01-00:00", 123, 1) +console.log(res1) // >>> 0 + +const res2 = await client.getBit("pings:2024-01-01-00:00", 123) +console.log(res2) // >>> 1 + +const res3 = await client.getBit("pings:2024-01-01-00:00", 456) +console.log(res3) // >>> 0 +// STEP_END + +// REMOVE_START +assert.equal(res1, 0) +// REMOVE_END + +// STEP_START bitcount +// HIDE_START +await client.setBit("pings:2024-01-01-00:00", 123, 1) +// HIDE_END +const res4 = await client.bitCount("pings:2024-01-01-00:00") +console.log(res4) // >>> 1 +// STEP_END +// REMOVE_START +assert.equal(res4, 1) +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/dt-bloom.js b/doctests/dt-bloom.js new file mode 100644 index 00000000000..d065937e763 --- /dev/null +++ b/doctests/dt-bloom.js @@ -0,0 +1,46 @@ +// EXAMPLE: bf_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START bloom +const res1 = await client.bf.reserve('bikes:models', 0.01, 1000); +console.log(res1); // >>> OK + +const res2 = await client.bf.add('bikes:models', 'Smoky Mountain Striker'); +console.log(res2); // >>> true + +const res3 = await client.bf.exists('bikes:models', 'Smoky Mountain Striker'); +console.log(res3); // >>> true + +const res4 = await client.bf.mAdd('bikes:models', [ + 'Rocky Mountain Racer', + 'Cloudy City Cruiser', + 'Windy City Wippet' +]); +console.log(res4); // >>> [true, true, true] + +const res5 = await client.bf.mExists('bikes:models', [ + 'Rocky Mountain Racer', + 'Cloudy City Cruiser', + 'Windy City Wippet' +]); +console.log(res5); // >>> [true, true, true] +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK') +assert.equal(res2, true) +assert.equal(res3, true) +assert.deepEqual(res4, [true, true, true]) +assert.deepEqual(res5, [true, true, true]) +await client.close(); +// REMOVE_END diff --git a/doctests/dt-cms.js b/doctests/dt-cms.js new file mode 100644 index 00000000000..b0d9fa68469 --- /dev/null +++ b/doctests/dt-cms.js @@ -0,0 +1,50 @@ +// EXAMPLE: cms_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START cms +const res1 = await client.cms.initByProb('bikes:profit', 0.001, 0.002); +console.log(res1); // >>> OK + +const res2 = await client.cms.incrBy('bikes:profit', { + item: 'Smoky Mountain Striker', + incrementBy: 100 +}); +console.log(res2); // >>> [100] + +const res3 = await client.cms.incrBy('bikes:profit', [ + { + item: 'Rocky Mountain Racer', + incrementBy: 200 + }, + { + item: 'Cloudy City Cruiser', + incrementBy: 150 + } +]); +console.log(res3); // >>> [200, 150] + +const res4 = await client.cms.query('bikes:profit', 'Smoky Mountain Striker'); +console.log(res4); // >>> [100] + +const res5 = await client.cms.info('bikes:profit'); +console.log(res5.width, res5.depth, res5.count); // >>> 2000 9 450 +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK') +assert.deepEqual(res2, [100]) +assert.deepEqual(res3, [200, 150]) +assert.deepEqual(res4, [100]) +assert.deepEqual(res5, { width: 2000, depth: 9, count: 450 }) +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/dt-cuckoo.js b/doctests/dt-cuckoo.js new file mode 100644 index 00000000000..4b11bca2345 --- /dev/null +++ b/doctests/dt-cuckoo.js @@ -0,0 +1,38 @@ +// EXAMPLE: cuckoo_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START cuckoo +const res1 = await client.cf.reserve('bikes:models', 1000000); +console.log(res1); // >>> OK + +const res2 = await client.cf.add('bikes:models', 'Smoky Mountain Striker'); +console.log(res2); // >>> true + +const res3 = await client.cf.exists('bikes:models', 'Smoky Mountain Striker'); +console.log(res3); // >>> true + +const res4 = await client.cf.exists('bikes:models', 'Terrible Bike Name'); +console.log(res4); // >>> false + +const res5 = await client.cf.del('bikes:models', 'Smoky Mountain Striker'); +console.log(res5); // >>> true +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK') +assert.equal(res2, true) +assert.equal(res3, true) +assert.equal(res4, false) +assert.equal(res5, true) +await client.close(); +// REMOVE_END diff --git a/doctests/dt-geo.js b/doctests/dt-geo.js new file mode 100644 index 00000000000..7ec9376a8a7 --- /dev/null +++ b/doctests/dt-geo.js @@ -0,0 +1,59 @@ +// EXAMPLE: geo_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del('bikes:rentable') +// REMOVE_END + +// STEP_START geoAdd +const res1 = await client.geoAdd('bikes:rentable', { + longitude: -122.27652, + latitude: 37.805186, + member: 'station:1' +}); +console.log(res1) // 1 + +const res2 = await client.geoAdd('bikes:rentable', { + longitude: -122.2674626, + latitude: 37.8062344, + member: 'station:2' +}); +console.log(res2) // 1 + +const res3 = await client.geoAdd('bikes:rentable', { + longitude: -122.2469854, + latitude: 37.8104049, + member: 'station:3' +}) +console.log(res3) // 1 +// STEP_END + +// REMOVE_START +assert.equal(res1, 1); +assert.equal(res2, 1); +assert.equal(res3, 1); +// REMOVE_END + +// STEP_START geoSearch +const res4 = await client.geoSearch( + 'bikes:rentable', { + longitude: -122.27652, + latitude: 37.805186, + }, + { radius: 5, + unit: 'km' + } +); +console.log(res4) // ['station:1', 'station:2', 'station:3'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res4, ['station:1', 'station:2', 'station:3']); +// REMOVE_END +await client.close() diff --git a/doctests/dt-hash.js b/doctests/dt-hash.js new file mode 100644 index 00000000000..e77d6fd7b95 --- /dev/null +++ b/doctests/dt-hash.js @@ -0,0 +1,98 @@ +// EXAMPLE: hash_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END +// STEP_START set_get_all +const res1 = await client.hSet( + 'bike:1', + { + 'model': 'Deimos', + 'brand': 'Ergonom', + 'type': 'Enduro bikes', + 'price': 4972, + } +) +console.log(res1) // 4 + +const res2 = await client.hGet('bike:1', 'model') +console.log(res2) // 'Deimos' + +const res3 = await client.hGet('bike:1', 'price') +console.log(res3) // '4972' + +const res4 = await client.hGetAll('bike:1') +console.log(res4) +/* +{ + brand: 'Ergonom', + model: 'Deimos', + price: '4972', + type: 'Enduro bikes' +} +*/ +// STEP_END + +// REMOVE_START +assert.equal(res1, 4); +assert.equal(res2, 'Deimos'); +assert.equal(res3, '4972'); +assert.deepEqual(res4, { + model: 'Deimos', + brand: 'Ergonom', + type: 'Enduro bikes', + price: '4972' +}); +// REMOVE_END + +// STEP_START hmGet +const res5 = await client.hmGet('bike:1', ['model', 'price']) +console.log(res5) // ['Deimos', '4972'] +// STEP_END + +// REMOVE_START +assert.deepEqual(Object.values(res5), ['Deimos', '4972']) +// REMOVE_END + +// STEP_START hIncrBy +const res6 = await client.hIncrBy('bike:1', 'price', 100) +console.log(res6) // 5072 +const res7 = await client.hIncrBy('bike:1', 'price', -100) +console.log(res7) // 4972 +// STEP_END + +// REMOVE_START +assert.equal(res6, 5072) +assert.equal(res7, 4972) +// REMOVE_END + +// STEP_START hIncrBy_hGet_hMget +const res11 = await client.hIncrBy('bike:1:stats', 'rides', 1) +console.log(res11) // 1 +const res12 = await client.hIncrBy('bike:1:stats', 'rides', 1) +console.log(res12) // 2 +const res13 = await client.hIncrBy('bike:1:stats', 'rides', 1) +console.log(res13) // 3 +const res14 = await client.hIncrBy('bike:1:stats', 'crashes', 1) +console.log(res14) // 1 +const res15 = await client.hIncrBy('bike:1:stats', 'owners', 1) +console.log(res15) // 1 +const res16 = await client.hGet('bike:1:stats', 'rides') +console.log(res16) // 3 +const res17 = await client.hmGet('bike:1:stats', ['crashes', 'owners']) +console.log(res17) // ['1', '1'] +// STEP_END + +// REMOVE_START +assert.equal(res11, 1); +assert.equal(res12, 2); +assert.equal(res13, 3); +assert.equal(res14, 1); +assert.equal(res15, 1); +assert.equal(res16, '3'); +assert.deepEqual(res17, ['1', '1']); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/dt-hll.js b/doctests/dt-hll.js new file mode 100644 index 00000000000..762ce5db15e --- /dev/null +++ b/doctests/dt-hll.js @@ -0,0 +1,38 @@ +// EXAMPLE: hll_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START pfadd +const res1 = await client.pfAdd('bikes', ['Hyperion', 'Deimos', 'Phoebe', 'Quaoar']); +console.log(res1); // >>> 1 + +const res2 = await client.pfCount('bikes'); +console.log(res2); // >>> 4 + +const res3 = await client.pfAdd('commuter_bikes', ['Salacia', 'Mimas', 'Quaoar']); +console.log(res3); // >>> 1 + +const res4 = await client.pfMerge('all_bikes', ['bikes', 'commuter_bikes']); +console.log(res4); // >>> OK + +const res5 = await client.pfCount('all_bikes'); +console.log(res5); // >>> 6 +// STEP_END + +// REMOVE_START +assert.equal(res1, 1) +assert.equal(res2, 4) +assert.equal(res3, 1) +assert.equal(res4, 'OK') +assert.equal(res5, 6) +await client.close(); +// REMOVE_END diff --git a/doctests/dt-json.js b/doctests/dt-json.js new file mode 100644 index 00000000000..00578f48f33 --- /dev/null +++ b/doctests/dt-json.js @@ -0,0 +1,425 @@ +// EXAMPLE: json_tutorial +// HIDE_START +import assert from 'assert'; +import { + createClient +} from 'redis'; + +const client = await createClient(); +await client.connect(); +// HIDE_END +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START set_get +const res1 = await client.json.set("bike", "$", '"Hyperion"'); +console.log(res1); // OK + +const res2 = await client.json.get("bike", { path: "$" }); +console.log(res2); // ['"Hyperion"'] + +const res3 = await client.json.type("bike", { path: "$" }); +console.log(res3); // [ 'string' ] +// STEP_END + +// REMOVE_START +assert.deepEqual(res2, ['"Hyperion"']); +// REMOVE_END + +// STEP_START str +const res4 = await client.json.strLen("bike", { path: "$" }); +console.log(res4) // [10] + +const res5 = await client.json.strAppend("bike", '" (Enduro bikes)"'); +console.log(res5) // 27 + +const res6 = await client.json.get("bike", { path: "$" }); +console.log(res6) // ['"Hyperion"" (Enduro bikes)"'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res6, ['"Hyperion"" (Enduro bikes)"']); +// REMOVE_END + +// STEP_START num +const res7 = await client.json.set("crashes", "$", 0); +console.log(res7) // OK + +const res8 = await client.json.numIncrBy("crashes", "$", 1); +console.log(res8) // [1] + +const res9 = await client.json.numIncrBy("crashes", "$", 1.5); +console.log(res9) // [2.5] + +const res10 = await client.json.numIncrBy("crashes", "$", -0.75); +console.log(res10) // [1.75] +// STEP_END + +// REMOVE_START +assert.deepEqual(res10, [1.75]) +// REMOVE_END + +// STEP_START arr +const res11 = await client.json.set("newbike", "$", ["Deimos", {"crashes": 0 }, null]); +console.log(res11); // OK + +const res12 = await client.json.get("newbike", { path: "$" }); +console.log(res12); // [[ 'Deimos', { crashes: 0 }, null ]] + +const res13 = await client.json.get("newbike", { path: "$[1].crashes" }); +console.log(res13); // [0] + +const res14 = await client.json.del("newbike", { path: "$.[-1]"} ); +console.log(res14); // 1 + +const res15 = await client.json.get("newbike", { path: "$" }); +console.log(res15); // [[ 'Deimos', { crashes: 0 } ]] +// STEP_END + +// REMOVE_START +assert.deepEqual(res15, [["Deimos", { + "crashes": 0 +}]]); +// REMOVE_END + +// STEP_START arr2 +const res16 = await client.json.set("riders", "$", []); +console.log(res16); // OK + +const res17 = await client.json.arrAppend("riders", "$", "Norem"); +console.log(res17); // [1] + +const res18 = await client.json.get("riders", { path: "$" }); +console.log(res18); // [[ 'Norem' ]] + +const res19 = await client.json.arrInsert("riders", "$", 1, "Prickett", "Royse", "Castilla"); +console.log(res19); // [4] + +const res20 = await client.json.get("riders", { path: "$" }); +console.log(res20); // [[ 'Norem', 'Prickett', 'Royse', 'Castilla' ]] + +const res21 = await client.json.arrTrim("riders", "$", 1, 1); +console.log(res21); // [1] + +const res22 = await client.json.get("riders", { path: "$" }); +console.log(res22); // [[ 'Prickett' ]] + +const res23 = await client.json.arrPop("riders", { path: "$" }); +console.log(res23); // [ 'Prickett' ] + +const res24 = await client.json.arrPop("riders", { path: "$" }); +console.log(res24); // [null] +// STEP_END + +// REMOVE_START +assert.deepEqual(res24, [null]); +// REMOVE_END + +// STEP_START obj +const res25 = await client.json.set( + "bike:1", "$", { + "model": "Deimos", + "brand": "Ergonom", + "price": 4972 + } +); +console.log(res25); // OK + +const res26 = await client.json.objLen("bike:1", { path: "$" }); +console.log(res26); // [3] + +const res27 = await client.json.objKeys("bike:1", { path: "$" }); +console.log(res27); // [['model', 'brand', 'price']] +// STEP_END + +// REMOVE_START +assert.deepEqual(res27, [ + ["model", "brand", "price"] +]); +// REMOVE_END + +// STEP_START set_bikes +// HIDE_START +const inventoryJSON = { + "inventory": { + "mountain_bikes": [{ + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there\u2019s room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.", + "price": 1920, + "specs": { + "material": "carbon", + "weight": 13.1 + }, + "colors": ["black", "silver"], + }, + { + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.", + "price": 2072, + "specs": { + "material": "aluminium", + "weight": 7.9 + }, + "colors": ["black", "white"], + }, + { + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + "price": 3264, + "specs": { + "material": "alloy", + "weight": 13.8 + }, + }, + ], + "commuter_bikes": [{ + "id": "bike:4", + "model": "Salacia", + "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance. It\u2019s for the rider who wants both efficiency and capability.", + "price": 1475, + "specs": { + "material": "aluminium", + "weight": 16.6 + }, + "colors": ["black", "silver"], + }, + { + "id": "bike:5", + "model": "Mimas", + "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.", + "price": 3941, + "specs": { + "material": "alloy", + "weight": 11.6 + }, + }, + ], + } +}; +// HIDE_END + +const res28 = await client.json.set("bikes:inventory", "$", inventoryJSON); +console.log(res28); // OK +// STEP_END + +// STEP_START get_bikes +const res29 = await client.json.get("bikes:inventory", { + path: "$.inventory.*" +}); +console.log(res29); +/* +[ + [ + { + id: 'bike:1', + model: 'Phoebe', + description: 'This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.', + price: 1920, + specs: [Object], + colors: [Array] + }, + { + id: 'bike:2', + model: 'Quaoar', + description: "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.", + price: 2072, + specs: [Object], + colors: [Array] + }, + { + id: 'bike:3', + model: 'Weywot', + description: "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + price: 3264, + specs: [Object] + } + ], + [ + { + id: 'bike:4', + model: 'Salacia', + description: 'This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance. It’s for the rider who wants both efficiency and capability.', + price: 1475, + specs: [Object], + colors: [Array] + }, + { + id: 'bike:5', + model: 'Mimas', + description: 'A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.', + price: 3941, + specs: [Object] + } + ] +] +*/ +// STEP_END + +// STEP_START get_mtnbikes +const res30 = await client.json.get("bikes:inventory", { + path: "$.inventory.mountain_bikes[*].model" +}); +console.log(res30); // ['Phoebe', 'Quaoar', 'Weywot'] + +const res31 = await client.json.get("bikes:inventory", { + path: '$.inventory["mountain_bikes"][*].model' +}); +console.log(res31); // ['Phoebe', 'Quaoar', 'Weywot'] + +const res32 = await client.json.get("bikes:inventory", { + path: "$..mountain_bikes[*].model" +}); +console.log(res32); // ['Phoebe', 'Quaoar', 'Weywot'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res30, ["Phoebe", "Quaoar", "Weywot"]); +assert.deepEqual(res31, ["Phoebe", "Quaoar", "Weywot"]); +assert.deepEqual(res32, ["Phoebe", "Quaoar", "Weywot"]); +// REMOVE_END + +// STEP_START get_models +const res33 = await client.json.get("bikes:inventory", { + path: "$..model" +}); +console.log(res33); // ['Phoebe', 'Quaoar', 'Weywot', 'Salacia', 'Mimas'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res33, ["Phoebe", "Quaoar", "Weywot", "Salacia", "Mimas"]); +// REMOVE_END + +// STEP_START get2mtnbikes +const res34 = await client.json.get("bikes:inventory", { + path: "$..mountain_bikes[0:2].model" +}); +console.log(res34); // ['Phoebe', 'Quaoar'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res34, ["Phoebe", "Quaoar"]); +// REMOVE_END + +// STEP_START filter1 +const res35 = await client.json.get("bikes:inventory", { + path: "$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]" +}); +console.log(res35); +/* +[ + { + id: 'bike:2', + model: 'Quaoar', + description: "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and teaawait client. All in all it's an impressive package for the price, making it very competitive.", + price: 2072, + specs: { material: 'aluminium', weight: 7.9 }, + colors: [ 'black', 'white' ] + } +] +*/ +// STEP_END + +// STEP_START filter2 +// names of bikes made from an alloy +const res36 = await client.json.get("bikes:inventory", { + path: "$..[?(@.specs.material == 'alloy')].model" +}); +console.log(res36); // ['Weywot', 'Mimas'] +// STEP_END +// REMOVE_START +assert.deepEqual(res36, ["Weywot", "Mimas"]); +// REMOVE_END + +// STEP_START filter3 +const res37 = await client.json.get("bikes:inventory", { + path: "$..[?(@.specs.material =~ '(?i)al')].model" +}); +console.log(res37); // ['Quaoar', 'Weywot', 'Salacia', 'Mimas'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res37, ["Quaoar", "Weywot", "Salacia", "Mimas"]); +// REMOVE_END + +// STEP_START filter4 +const res37a = await client.json.set( + 'bikes:inventory', + '$.inventory.mountain_bikes[0].regex_pat', + '(?i)al' +); + +const res37b = await client.json.set( + 'bikes:inventory', + '$.inventory.mountain_bikes[1].regex_pat', + '(?i)al' +); + +const res37c = await client.json.set( + 'bikes:inventory', + '$.inventory.mountain_bikes[2].regex_pat', + '(?i)al' +); + +const res37d = await client.json.get( + 'bikes:inventory', + { path: '$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model' } +); +console.log(res37d); // ['Quaoar', 'Weywot'] +// STEP_END + +// STEP_START update_bikes +const res38 = await client.json.get("bikes:inventory", { + path: "$..price" +}); +console.log(res38); // [1920, 2072, 3264, 1475, 3941] + +const res39 = await client.json.numIncrBy("bikes:inventory", "$..price", -100); +console.log(res39); // [1820, 1972, 3164, 1375, 3841] + +const res40 = await client.json.numIncrBy("bikes:inventory", "$..price", 100); +console.log(res40); // [1920, 2072, 3264, 1475, 3941] +// STEP_END + +// REMOVE_START +assert.deepEqual(res40.sort(), [1475, 1920, 2072, 3264, 3941]); +// REMOVE_END + +// STEP_START update_filters1 +const res40a = await client.json.set( + 'bikes:inventory', + '$.inventory.*[?(@.price<2000)].price', + 1500 +); + +// Get all prices from the inventory +const res40b = await client.json.get( + 'bikes:inventory', + { path: "$..price" } +); +console.log(res40b); // [1500, 2072, 3264, 1500, 3941] +// STEP_END + +// STEP_START update_filters2 +const res41 = await client.json.arrAppend( + "bikes:inventory", "$.inventory.*[?(@.price<2000)].colors", "pink" +); +console.log(res41); // [3, 3] + +const res42 = await client.json.get("bikes:inventory", { + path: "$..[*].colors" +}); +console.log(res42); // [['black', 'silver', 'pink'], ['black', 'white'], ['black', 'silver', 'pink']] +// STEP_END + +// REMOVE_START +assert.deepEqual(res42, [ + ["black", "silver", "pink"], + ["black", "white"], + ["black", "silver", "pink"], +]); +await client.close(); +// REMOVE_END diff --git a/doctests/dt-list.js b/doctests/dt-list.js new file mode 100644 index 00000000000..a2d0fb86c66 --- /dev/null +++ b/doctests/dt-list.js @@ -0,0 +1,329 @@ +// EXAMPLE: list_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END +// REMOVE_START +await client.del('bikes:repairs'); +await client.del('bikes:finished'); +// REMOVE_END + +// STEP_START queue +const res1 = await client.lPush('bikes:repairs', 'bike:1'); +console.log(res1); // 1 + +const res2 = await client.lPush('bikes:repairs', 'bike:2'); +console.log(res2); // 2 + +const res3 = await client.rPop('bikes:repairs'); +console.log(res3); // bike:1 + +const res4 = await client.rPop('bikes:repairs'); +console.log(res4); // bike:2 +// STEP_END + +// REMOVE_START +assert.equal(res1, 1); +assert.equal(res2, 2); +assert.equal(res3, 'bike:1'); +assert.equal(res4, 'bike:2'); +// REMOVE_END + +// STEP_START stack +const res5 = await client.lPush('bikes:repairs', 'bike:1'); +console.log(res5); // 1 + +const res6 = await client.lPush('bikes:repairs', 'bike:2'); +console.log(res6); // 2 + +const res7 = await client.lPop('bikes:repairs'); +console.log(res7); // bike:2 + +const res8 = await client.lPop('bikes:repairs'); +console.log(res8); // bike:1 +// STEP_END + +// REMOVE_START +assert.equal(res5, 1); +assert.equal(res6, 2); +assert.equal(res7, 'bike:2'); +assert.equal(res8, 'bike:1'); +// REMOVE_END + +// STEP_START lLen +const res9 = await client.lLen('bikes:repairs'); +console.log(res9); // 0 +// STEP_END + +// REMOVE_START +assert.equal(res9, 0); +// REMOVE_END + +// STEP_START lMove_lRange +const res10 = await client.lPush('bikes:repairs', 'bike:1'); +console.log(res10); // 1 + +const res11 = await client.lPush('bikes:repairs', 'bike:2'); +console.log(res11); // 2 + +const res12 = await client.lMove('bikes:repairs', 'bikes:finished', 'LEFT', 'LEFT'); +console.log(res12); // 'bike:2' + +const res13 = await client.lRange('bikes:repairs', 0, -1); +console.log(res13); // ['bike:1'] + +const res14 = await client.lRange('bikes:finished', 0, -1); +console.log(res14); // ['bike:2'] +// STEP_END + +// REMOVE_START +assert.equal(res10, 1); +assert.equal(res11, 2); +assert.equal(res12, 'bike:2'); +assert.deepEqual(res13, ['bike:1']); +assert.deepEqual(res14, ['bike:2']); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START lPush_rPush +const res15 = await client.rPush('bikes:repairs', 'bike:1'); +console.log(res15); // 1 + +const res16 = await client.rPush('bikes:repairs', 'bike:2'); +console.log(res16); // 2 + +const res17 = await client.lPush('bikes:repairs', 'bike:important_bike'); +console.log(res17); // 3 + +const res18 = await client.lRange('bikes:repairs', 0, -1); +console.log(res18); // ['bike:important_bike', 'bike:1', 'bike:2'] +// STEP_END + +// REMOVE_START +assert.equal(res15, 1); +assert.equal(res16, 2); +assert.equal(res17, 3); +assert.deepEqual(res18, ['bike:important_bike', 'bike:1', 'bike:2']); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START variadic +const res19 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']); +console.log(res19); // 3 + +const res20 = await client.lPush( + 'bikes:repairs', ['bike:important_bike', 'bike:very_important_bike'] +); +console.log(res20); // 5 + +const res21 = await client.lRange('bikes:repairs', 0, -1); +console.log(res21); // ['bike:very_important_bike', 'bike:important_bike', 'bike:1', 'bike:2', 'bike:3'] +// STEP_END + +// REMOVE_START +assert.equal(res19, 3); +assert.equal(res20, 5); +assert.deepEqual(res21, [ + 'bike:very_important_bike', + 'bike:important_bike', + 'bike:1', + 'bike:2', + 'bike:3', +]); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START lPop_rPop +const res22 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']); +console.log(res22); // 3 + +const res23 = await client.rPop('bikes:repairs'); +console.log(res23); // 'bike:3' + +const res24 = await client.lPop('bikes:repairs'); +console.log(res24); // 'bike:1' + +const res25 = await client.rPop('bikes:repairs'); +console.log(res25); // 'bike:2' + +const res26 = await client.rPop('bikes:repairs'); +console.log(res26); // null +// STEP_END + +// REMOVE_START +assert.deepEqual(res22, 3); +assert.equal(res23, 'bike:3'); +assert.equal(res24, 'bike:1'); +assert.equal(res25, 'bike:2'); +assert.equal(res26, null); +// REMOVE_END + +// STEP_START lTrim +const res27 = await client.lPush( + 'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5'] +); +console.log(res27); // 5 + +const res28 = await client.lTrim('bikes:repairs', 0, 2); +console.log(res28); // OK + +const res29 = await client.lRange('bikes:repairs', 0, -1); +console.log(res29); // ['bike:5', 'bike:4', 'bike:3'] +// STEP_END + +// REMOVE_START +assert.equal(res27, 5); +assert.equal(res28, 'OK'); +assert.deepEqual(res29, ['bike:5', 'bike:4', 'bike:3']); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START lTrim_end_of_list +const res27eol = await client.rPush( + 'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5'] +); +console.log(res27eol); // 5 + +const res28eol = await client.lTrim('bikes:repairs', -3, -1); +console.log(res28eol); // 'OK' + +const res29eol = await client.lRange('bikes:repairs', 0, -1); +console.log(res29eol); // ['bike:3', 'bike:4', 'bike:5'] +// STEP_END + +// REMOVE_START +assert.equal(res27eol, 5); +assert.equal(res28eol, 'OK'); +assert.deepEqual(res29eol, ['bike:3', 'bike:4', 'bike:5']); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START brPop +const res31 = await client.rPush('bikes:repairs', ['bike:1', 'bike:2']); +console.log(res31); // 2 + +const res32 = await client.brPop('bikes:repairs', 1); +console.log(res32); // { key: 'bikes:repairs', element: 'bike:2' } + +const res33 = await client.brPop('bikes:repairs', 1); +console.log(res33); // { key: 'bikes:repairs', element: 'bike:1' } + +const res34 = await client.brPop('bikes:repairs', 1); +console.log(res34); // null +// STEP_END + +// REMOVE_START +assert.equal(res31, 2); +assert.deepEqual(res32, { key: 'bikes:repairs', element: 'bike:2' }); +assert.deepEqual(res33, { key: 'bikes:repairs', element: 'bike:1' }); +assert.equal(res34, null); +await client.del('bikes:repairs'); +await client.del('new_bikes'); +// REMOVE_END + +// STEP_START rule_1 +const res35 = await client.del('new_bikes'); +console.log(res35); // 0 + +const res36 = await client.lPush('new_bikes', ['bike:1', 'bike:2', 'bike:3']); +console.log(res36); // 3 +// STEP_END + +// REMOVE_START +assert.equal(res35, 0); +assert.equal(res36, 3); +await client.del('new_bikes'); +// REMOVE_END + +// STEP_START rule_1.1 +const res37 = await client.set('new_bikes', 'bike:1'); +console.log(res37); // 'OK' + +const res38 = await client.type('new_bikes'); +console.log(res38); // 'string' + +try { + const res39 = await client.lPush('new_bikes', 'bike:2', 'bike:3'); + // redis.exceptions.ResponseError: + // [SimpleError: WRONGTYPE Operation against a key holding the wrong kind of value] +} +catch(e){ + console.log(e); +} +// STEP_END + +// REMOVE_START +assert.equal(res37, 'OK'); +assert.equal(res38, 'string'); +await client.del('new_bikes'); +// REMOVE_END + +// STEP_START rule_2 +await client.lPush('bikes:repairs', ['bike:1', 'bike:2', 'bike:3']); +console.log(res36); // 3 + +const res40 = await client.exists('bikes:repairs') +console.log(res40); // 1 + +const res41 = await client.lPop('bikes:repairs'); +console.log(res41); // 'bike:3' + +const res42 = await client.lPop('bikes:repairs'); +console.log(res42); // 'bike:2' + +const res43 = await client.lPop('bikes:repairs'); +console.log(res43); // 'bike:1' + +const res44 = await client.exists('bikes:repairs'); +console.log(res44); // 0 +// STEP_END + +// REMOVE_START +assert.equal(res40, 1); +assert.equal(res41, 'bike:3'); +assert.equal(res42, 'bike:2'); +assert.equal(res43, 'bike:1'); +assert.equal(res44, 0); +await client.del('bikes:repairs'); +// REMOVE_END + +// STEP_START rule_3 +const res45 = await client.del('bikes:repairs'); +console.log(res45); // 0 + +const res46 = await client.lLen('bikes:repairs'); +console.log(res46); // 0 + +const res47 = await client.lPop('bikes:repairs'); +console.log(res47); // null +// STEP_END + +// REMOVE_START +assert.equal(res45, 0); +assert.equal(res46, 0); +assert.equal(res47, null); +// REMOVE_END + +// STEP_START lTrim.1 +const res48 = await client.lPush( + 'bikes:repairs', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5'] +); +console.log(res48); // 5 + +const res49 = await client.lTrim('bikes:repairs', 0, 2); +console.log(res49); // 'OK' + +const res50 = await client.lRange('bikes:repairs', 0, -1); +console.log(res50); // ['bike:5', 'bike:4', 'bike:3'] +// STEP_END + +// REMOVE_START +assert.equal(res48, 5); +assert.equal(res49, 'OK'); +assert.deepEqual(res50, ['bike:5', 'bike:4', 'bike:3']); +await client.del('bikes:repairs'); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/dt-set.js b/doctests/dt-set.js new file mode 100644 index 00000000000..e3d34096399 --- /dev/null +++ b/doctests/dt-set.js @@ -0,0 +1,176 @@ +// EXAMPLE: sets_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END +// REMOVE_START +await client.del('bikes:racing:france') +await client.del('bikes:racing:usa') +// REMOVE_END + +// STEP_START sAdd +const res1 = await client.sAdd('bikes:racing:france', 'bike:1') +console.log(res1) // >>> 1 + +const res2 = await client.sAdd('bikes:racing:france', 'bike:1') +console.log(res2) // >>> 0 +const res3 = await client.sAdd('bikes:racing:france', ['bike:2', 'bike:3']) +console.log(res3) // >>> 2 +const res4 = await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4']) +console.log(res4) // >>> 2 +// STEP_END + +// REMOVE_START +assert.equal(res1, 1) +assert.equal(res2, 0) +assert.equal(res3, 2) +assert.equal(res4, 2) +// REMOVE_END + +// STEP_START sIsMember +// HIDE_START +await client.del('bikes:racing:france') +await client.del('bikes:racing:usa') +await client.sAdd('bikes:racing:france', 'bike:1', 'bike:2', 'bike:3') +await client.sAdd('bikes:racing:usa', 'bike:1', 'bike:4') +// HIDE_END +const res5 = await client.sIsMember('bikes:racing:usa', 'bike:1') +console.log(res5) // >>> 1 + +const res6 = await client.sIsMember('bikes:racing:usa', 'bike:2') +console.log(res6) // >>> 0 +// STEP_END + +// REMOVE_START +assert.equal(res5, 1) +assert.equal(res6, 0) +// REMOVE_END + +// STEP_START sinster +// HIDE_START +await client.del('bikes:racing:france') +await client.del('bikes:racing:usa') +await client.sAdd('bikes:racing:france', 'bike:1', 'bike:2', 'bike:3') +await client.sAdd('bikes:racing:usa', 'bike:1', 'bike:4') +// HIDE_END +const res7 = await client.sInter('bikes:racing:france', 'bikes:racing:usa') +console.log(res7) // >>> {'bike:1'} +// STEP_END + +// REMOVE_START +assert.deepEqual(res7, [ 'bike:1' ]) +// REMOVE_END + +// STEP_START sCard +// HIDE_START +await client.del('bikes:racing:france') +await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) +// HIDE_END +const res8 = await client.sCard('bikes:racing:france') +console.log(res8) // >>> 3 +// STEP_END + +// REMOVE_START +assert.equal(res8, 3) +await client.del('bikes:racing:france') +// REMOVE_END + +// STEP_START sAdd_sMembers +const res9 = await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) +console.log(res9) // >>> 3 + +const res10 = await client.sMembers('bikes:racing:france') +console.log(res10) // >>> ['bike:1', 'bike:2', 'bike:3'] +// STEP_END + +// REMOVE_START +assert.equal(res9, 3) +assert.deepEqual(res10.sort(), ['bike:1', 'bike:2', 'bike:3']) +// REMOVE_END + +// STEP_START smIsMember +const res11 = await client.sIsMember('bikes:racing:france', 'bike:1') +console.log(res11) // >>> 1 + +const res12 = await client.smIsMember('bikes:racing:france', ['bike:2', 'bike:3', 'bike:4']) +console.log(res12) // >>> [1, 1, 0] +// STEP_END + +// REMOVE_START +assert.equal(res11, 1) +assert.deepEqual(res12, [1, 1, 0]) +// REMOVE_END + +// STEP_START sDiff +await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) +await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4']) +const res13 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa']) +console.log(res13) // >>> [ 'bike:2', 'bike:3' ] +// STEP_END + +// REMOVE_START +assert.deepEqual(res13.sort(), ['bike:2', 'bike:3'].sort()) +await client.del('bikes:racing:france') +await client.del('bikes:racing:usa') +// REMOVE_END + +// STEP_START multisets +await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) +await client.sAdd('bikes:racing:usa', ['bike:1', 'bike:4']) +await client.sAdd('bikes:racing:italy', ['bike:1', 'bike:2', 'bike:3', 'bike:4']) + +const res14 = await client.sInter( + ['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy'] +) +console.log(res14) // >>> ['bike:1'] + +const res15 = await client.sUnion( + ['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy'] +) +console.log(res15) // >>> ['bike:1', 'bike:2', 'bike:3', 'bike:4'] + +const res16 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa', 'bikes:racing:italy']) +console.log(res16) // >>> [] + +const res17 = await client.sDiff(['bikes:racing:usa', 'bikes:racing:france']) +console.log(res17) // >>> ['bike:4'] + +const res18 = await client.sDiff(['bikes:racing:france', 'bikes:racing:usa']) +console.log(res18) // >>> ['bike:2', 'bike:3'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res14, ['bike:1']) +assert.deepEqual(res15.sort(), ['bike:1', 'bike:2', 'bike:3', 'bike:4']) +assert.deepEqual(res16, []) +assert.deepEqual(res17, ['bike:4']) +assert.deepEqual(res18.sort(), ['bike:2', 'bike:3'].sort()) +await client.del('bikes:racing:france') +await client.del('bikes:racing:usa') +await client.del('bikes:racing:italy') +// REMOVE_END + +// STEP_START sRem +await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3', 'bike:4', 'bike:5']) + +const res19 = await client.sRem('bikes:racing:france', 'bike:1') +console.log(res19) // >>> 1 + +const res20 = await client.sPop('bikes:racing:france') +console.log(res20) // >>> bike:3 or other random value + +const res21 = await client.sMembers('bikes:racing:france') +console.log(res21) // >>> ['bike:2', 'bike:4', 'bike:5']; depends on previous result + +const res22 = await client.sRandMember('bikes:racing:france') +console.log(res22) // >>> bike:4 or other random value +// STEP_END + +// REMOVE_START +assert.equal(res19, 1) +await client.close() +// none of the other results are deterministic +// REMOVE_END diff --git a/doctests/dt-ss.js b/doctests/dt-ss.js new file mode 100644 index 00000000000..bba85b11580 --- /dev/null +++ b/doctests/dt-ss.js @@ -0,0 +1,162 @@ +// EXAMPLE: ss_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START zadd +const res1 = await client.zAdd('racer_scores', { score: 10, value: 'Norem' }); +console.log(res1); // >>> 1 + +const res2 = await client.zAdd('racer_scores', { score: 12, value: 'Castilla' }); +console.log(res2); // >>> 1 + +const res3 = await client.zAdd('racer_scores', [ + { score: 8, value: 'Sam-Bodden' }, + { score: 10, value: 'Royce' }, + { score: 6, value: 'Ford' }, + { score: 14, value: 'Prickett' }, + { score: 12, value: 'Castilla' } +]); +console.log(res3); // >>> 4 +// STEP_END + +// REMOVE_START +assert.equal(res1, 1) +assert.equal(res2, 1) +assert.equal(res3, 4) +// REMOVE_END + +// REMOVE_START +const count = await client.zCard('racer_scores'); +console.assert(count === 6); +// REMOVE_END + +// STEP_START zrange +const res4 = await client.zRange('racer_scores', 0, -1); +console.log(res4); // >>> ['Ford', 'Sam-Bodden', 'Norem', 'Royce', 'Castilla', 'Prickett'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res4, ['Ford', 'Sam-Bodden', 'Norem', 'Royce', 'Castilla', 'Prickett']) +// REMOVE_END + +// STEP_START zrange_withscores +const res6 = await client.zRangeWithScores('racer_scores', 0, -1); +console.log(res6); +// >>> [ +// { value: 'Ford', score: 6 }, { value: 'Sam-Bodden', score: 8 }, +// { value: 'Norem', score: 10 }, { value: 'Royce', score: 10 }, +// { value: 'Castilla', score: 12 }, { value: 'Prickett', score: 14 } +// ] +// STEP_END + +// REMOVE_START +assert.deepEqual(res6, [ { value: 'Ford', score: 6 }, { value: 'Sam-Bodden', score: 8 }, { value: 'Norem', score: 10 }, { value: 'Royce', score: 10 }, { value: 'Castilla', score: 12 }, { value: 'Prickett', score: 14 } ] +) +// REMOVE_END + +// STEP_START zrangebyscore +const res7 = await client.zRangeByScore('racer_scores', '-inf', 10); +console.log(res7); // >>> ['Ford', 'Sam-Bodden', 'Norem', 'Royce'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res7, ['Ford', 'Sam-Bodden', 'Norem', 'Royce']) +// REMOVE_END + +// STEP_START zremrangebyscore +const res8 = await client.zRem('racer_scores', 'Castilla'); +console.log(res8); // >>> 1 + +const res9 = await client.zRemRangeByScore('racer_scores', '-inf', 9); +console.log(res9); // >>> 2 + +// REMOVE_START +assert.equal(res8, 1) +assert.equal(res9, 2) +// REMOVE_END + +const res10 = await client.zRange('racer_scores', 0, -1); +console.log(res10); // >>> ['Norem', 'Royce', 'Prickett'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res10, ['Norem', 'Royce', 'Prickett']) +// REMOVE_END + +// REMOVE_START +const count2 = await client.zCard('racer_scores'); +console.assert(count2 === 3); +// REMOVE_END + +// STEP_START zrank +const res11 = await client.zRank('racer_scores', 'Norem'); +console.log(res11); // >>> 0 + +const res12 = await client.zRevRank('racer_scores', 'Norem'); +console.log(res12); // >>> 2 +// STEP_END + +// STEP_START zadd_lex +const res13 = await client.zAdd('racer_scores', [ + { score: 0, value: 'Norem' }, + { score: 0, value: 'Sam-Bodden' }, + { score: 0, value: 'Royce' }, + { score: 0, value: 'Ford' }, + { score: 0, value: 'Prickett' }, + { score: 0, value: 'Castilla' } +]); +console.log(res13); // >>> 3 + +// REMOVE_START +assert.equal(count2, 3) +assert.equal(res11, 0) +assert.equal(res12, 2) +assert.equal(res13, 3) +// REMOVE_END + +const res14 = await client.zRange('racer_scores', 0, -1); +console.log(res14); // >>> ['Castilla', 'Ford', 'Norem', 'Prickett', 'Royce', 'Sam-Bodden'] + +const res15 = await client.zRangeByLex('racer_scores', '[A', '[L'); +console.log(res15); // >>> ['Castilla', 'Ford'] +// STEP_END + +// REMOVE_START +assert.deepEqual(res14, ['Castilla', 'Ford', 'Norem', 'Prickett', 'Royce', 'Sam-Bodden']) +assert.deepEqual(res15, ['Castilla', 'Ford']) +// REMOVE_END + +// STEP_START leaderboard +const res16 = await client.zAdd('racer_scores', { score: 100, value: 'Wood' }); +console.log(res16); // >>> 1 + +const res17 = await client.zAdd('racer_scores', { score: 100, value: 'Henshaw' }); +console.log(res17); // >>> 1 + +const res18 = await client.zAdd('racer_scores', { score: 150, value: 'Henshaw' }, { nx: true }); +console.log(res18); // >>> 0 + +const res19 = await client.zIncrBy('racer_scores', 50, 'Wood'); +console.log(res19); // >>> 150.0 + +const res20 = await client.zIncrBy('racer_scores', 50, 'Henshaw'); +console.log(res20); // >>> 200.0 +// STEP_END + +// REMOVE_START +assert.equal(res16, 1) +assert.equal(res17, 1) +assert.equal(res18, 0) +assert.equal(res19, 150.0) +assert.equal(res20, 200.0) +await client.close(); +// REMOVE_END diff --git a/doctests/dt-streams.js b/doctests/dt-streams.js new file mode 100644 index 00000000000..00768654730 --- /dev/null +++ b/doctests/dt-streams.js @@ -0,0 +1,366 @@ +// EXAMPLE: stream_tutorial +// HIDE_START +import assert from 'assert'; +import { + createClient +} from 'redis'; + +const client = await createClient(); +await client.connect(); +// HIDE_END +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START xAdd +const res1 = await client.xAdd( + 'race:france', '*', { + 'rider': 'Castilla', + 'speed': '30.2', + 'position': '1', + 'location_id': '1' + } +); +console.log(res1); // >>> 1700073067968-0 N.B. actual values will differ from these examples + +const res2 = await client.xAdd( + 'race:france', '*', { + 'rider': 'Norem', + 'speed': '28.8', + 'position': '3', + 'location_id': '1' + }, +); +console.log(res2); // >>> 1692629594113-0 + +const res3 = await client.xAdd( + 'race:france', '*', { + 'rider': 'Prickett', + 'speed': '29.7', + 'position': '2', + 'location_id': '1' + }, +); +console.log(res3); // >>> 1692629613374-0 +// STEP_END + +// REMOVE_START +assert.equal(await client.xLen('race:france'), 3); +// REMOVE_END + +// STEP_START xRange +const res4 = await client.xRange('race:france', '1691765278160-0', '+', {COUNT: 2}); +console.log(res4); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] +// STEP_END + +// STEP_START xread_block +const res5 = await client.xRead({ + key: 'race:france', + id: '0-0' +}, { + COUNT: 100, + BLOCK: 300 +}); +console.log(res5); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }] }] +// STEP_END + +// STEP_START xAdd_2 +const res6 = await client.xAdd( + 'race:france', '*', { + 'rider': 'Castilla', + 'speed': '29.9', + 'position': '1', + 'location_id': '2' + } +); +console.log(res6); // >>> 1692629676124-0 +// STEP_END + +// STEP_START xlen +const res7 = await client.xLen('race:france'); +console.log(res7); // >>> 4 +// STEP_END + + +// STEP_START xAdd_id +const res8 = await client.xAdd('race:usa', '0-1', { + 'racer': 'Castilla' +}); +console.log(res8); // >>> 0-1 + +const res9 = await client.xAdd('race:usa', '0-2', { + 'racer': 'Norem' +}); +console.log(res9); // >>> 0-2 +// STEP_END + +// STEP_START xAdd_bad_id +try { + const res10 = await client.xAdd('race:usa', '0-1', { + 'racer': 'Prickett' + }); + console.log(res10); // >>> 0-1 +} catch (error) { + console.error(error); // >>> [SimpleError: ERR The ID specified in XADD is equal or smaller than the target stream top item] +} +// STEP_END + +// STEP_START xadd_7 +const res11a = await client.xAdd('race:usa', '0-*', { racer: 'Norem' }); +console.log(res11a); // >>> 0-3 +// STEP_END + +// STEP_START xRange_all +const res11 = await client.xRange('race:france', '-', '+'); +console.log(res11); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] +// STEP_END + +// STEP_START xRange_time +const res12 = await client.xRange('race:france', '1692629576965', '1692629576967'); +console.log(res12); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }] +// STEP_END + +// STEP_START xRange_step_1 +const res13 = await client.xRange('race:france', '-', '+', {COUNT: 2}); +console.log(res13); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] +// STEP_END + +// STEP_START xRange_step_2 +const res14 = await client.xRange('race:france', '(1692629594113-0', '+', {COUNT: 2}); +console.log(res14); // >>> [{ id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] +// STEP_END + +// STEP_START xRange_empty +const res15 = await client.xRange('race:france', '(1692629676124-0', '+', {COUNT: 2}); +console.log(res15); // >>> [] +// STEP_END + +// STEP_START xrevrange +const res16 = await client.xRevRange('race:france', '+', '-', {COUNT: 1}); +console.log( + res16 +); // >>> [{ id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] +// STEP_END + +// STEP_START xread +const res17 = await client.xRead({ + key: 'race:france', + id: '0-0' +}, { + COUNT: 2 +}); +console.log(res17); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] }] +// STEP_END + +// STEP_START xgroup_create +const res18 = await client.xGroupCreate('race:france', 'france_riders', '$'); +console.log(res18); // >>> OK +// STEP_END + +// STEP_START xgroup_create_mkstream +const res19 = await client.xGroupCreate('race:italy', 'italy_riders', '$', { + MKSTREAM: true +}); +console.log(res19); // >>> OK +// STEP_END + +// STEP_START xgroup_read +await client.xAdd('race:italy', '*', { + 'rider': 'Castilla' +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Royce' +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Sam-Bodden' +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Prickett' +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Norem' +}); + +const res20 = await client.xReadGroup( + 'italy_riders', + 'Alice', { + key: 'race:italy', + id: '>' + }, { + COUNT: 1 + } +); +console.log(res20); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }] +// STEP_END + +// STEP_START xgroup_read_id +const res21 = await client.xReadGroup( + 'italy_riders', + 'Alice', { + key: 'race:italy', + id: '0' + }, { + COUNT: 1 + } +); +console.log(res21); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }] +// STEP_END + +// STEP_START xack +const res22 = await client.xAck('race:italy', 'italy_riders', '1692629925771-0') +console.log(res22); // >>> 1 + +const res23 = await client.xReadGroup( + 'italy_riders', + 'Alice', { + key: 'race:italy', + id: '0' + }, { + COUNT: 1 + } +); +console.log(res23); // >>> [{ name: 'race:italy', messages: [] }] +// STEP_END + +// STEP_START xgroup_read_bob +const res24 = await client.xReadGroup( + 'italy_riders', + 'Bob', { + key: 'race:italy', + id: '>' + }, { + COUNT: 2 + } +); +console.log(res24); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }] }] +// STEP_END + +// STEP_START xpending +const res25 = await client.xPending('race:italy', 'italy_riders'); +console.log(res25); // >>> {'pending': 2, 'firstId': '1692629925789-0', 'lastId': '1692629925790-0', 'consumers': [{'name': 'Bob', 'deliveriesCounter': 2}]} +// STEP_END + +// STEP_START xpending_plus_minus +const res26 = await client.xPendingRange('race:italy', 'italy_riders', '-', '+', 10); +console.log(res26); // >>> [{'id': '1692629925789-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter:': 1}, {'id': '1692629925790-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter': 1}] +// STEP_END + +// STEP_START xRange_pending +const res27 = await client.xRange('race:italy', '1692629925789-0', '1692629925789-0'); +console.log(res27); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }] +// STEP_END + +// STEP_START xclaim +const res28 = await client.xClaim( + 'race:italy', 'italy_riders', 'Alice', 60000, ['1692629925789-0'] +); +console.log(res28); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }] +// STEP_END + +// STEP_START xautoclaim +const res29 = await client.xAutoClaim('race:italy', 'italy_riders', 'Alice', 1, '0-0', { + COUNT: 1 +}); +console.log(res29); // >>> { nextId: '1692629925790-0', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }], deletedMessages: [] } +// STEP_END + +// STEP_START xautoclaim_cursor +const res30 = await client.xAutoClaim( + 'race:italy', 'italy_riders', 'Alice', 1, '(1692629925789-0', + { + COUNT: 1 + } +); +console.log(res30); // >>> { nextId: '0-0', messages: [{ id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }], deletedMessages: [] } +// STEP_END + +// STEP_START xinfo +const res31 = await client.xInfoStream('race:italy'); +console.log(res31); // >>> { length: 5, 'radix-tree-keys': 1, 'radix-tree-nodes': 2, 'last-generated-id': '1692629926436-0', 'max-deleted-entry-id': '0-0', 'entries-added': 5, 'recorded-first-entry-id': '1692629925771-0', groups: 1, 'first-entry': { id: '1692629925771-0', message: { rider: 'Castilla' } }, 'last-entry': { id: '1692629926436-0', message: { rider: 'Norem' } } } +// STEP_END + +// STEP_START xinfo_groups +const res32 = await client.xInfoGroups('race:italy'); +console.log(res32); // >>> [{ name: 'italy_riders', consumers: 2, pending: 3, 'last-delivered-id': '1692629925790-0', 'entries-read': 3, lag: 2 }] +// STEP_END + +// STEP_START xinfo_consumers +const res33 = await client.xInfoConsumers('race:italy', 'italy_riders'); +console.log(res33); // >>> [{ name: 'Alice', pending: 3, idle: 170582, inactive: 170582 }, { name: 'Bob', pending: 0, idle: 489404, inactive: 489404 }] +// STEP_END + +// STEP_START maxlen +await client.xAdd('race:italy', '*', { + 'rider': 'Jones' +}, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 2 + } +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Wood' +}, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 2 + } +}); +await client.xAdd('race:italy', '*', { + 'rider': 'Henshaw' +}, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 2 + } +}); + +const res34 = await client.xLen('race:italy'); +console.log(res34); // >>> 8 + +const res35 = await client.xRange('race:italy', '-', '+'); +console.log(res35); // >>> [{ id: '1692629925771-0', message: { rider: 'Castilla' } }, { id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }, { id: '1692629925791-0', message: { rider: 'Prickett' } }, { id: '1692629926436-0', message: { rider: 'Norem' } }, { id: '1692630612602-0', message: { rider: 'Jones' } }, { id: '1692630641947-0', message: { rider: 'Wood' } }, { id: '1692630648281-0', message: { rider: 'Henshaw' } }] + +await client.xAdd('race:italy', '*', { + 'rider': 'Smith' +}, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '=', + threshold: 2 + } +}); + +const res36 = await client.xRange('race:italy', '-', '+'); +console.log(res36); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }] +// STEP_END + +// STEP_START xTrim +const res37 = await client.xTrim('race:italy', 'MAXLEN', 10, { + strategyModifier: '=', +}); +console.log(res37); // >>> 0 +// STEP_END + +// STEP_START xTrim2 +const res38 = await client.xTrim('race:italy', "MAXLEN", 10); +console.log(res38); // >>> 0 +// STEP_END + +// STEP_START xDel +const res39 = await client.xRange('race:italy', '-', '+'); +console.log(res39); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }] + +const res40 = await client.xDel('race:italy', '1692631018238-0'); +console.log(res40); // >>> 1 + +const res41 = await client.xRange('race:italy', '-', '+'); +console.log(res41); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }] +// STEP_END + +// REMOVE_START +await client.quit(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/dt-string.js b/doctests/dt-string.js new file mode 100644 index 00000000000..6f77709abfa --- /dev/null +++ b/doctests/dt-string.js @@ -0,0 +1,68 @@ +// EXAMPLE: set_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START set_get +const res1 = await client.set("bike:1", "Deimos"); +console.log(res1); // OK +const res2 = await client.get("bike:1"); +console.log(res2); // Deimos +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK'); +assert.equal(res2, 'Deimos'); +// REMOVE_END + +// STEP_START setnx_xx +const res3 = await client.set("bike:1", "bike", {'NX': true}); +console.log(res3); // null +console.log(await client.get("bike:1")); // Deimos +const res4 = await client.set("bike:1", "bike", {'XX': true}); +console.log(res4); // OK +// STEP_END + +// REMOVE_START +assert.equal(res3, null); +assert.equal(res4, 'OK'); +// REMOVE_END + +// STEP_START mset +const res5 = await client.mSet([ + ["bike:1", "Deimos"], + ["bike:2", "Ares"], + ["bike:3", "Vanth"] +]); + +console.log(res5); // OK +const res6 = await client.mGet(["bike:1", "bike:2", "bike:3"]); +console.log(res6); // ['Deimos', 'Ares', 'Vanth'] +// STEP_END + +// REMOVE_START +assert.equal(res5, 'OK'); +assert.deepEqual(res6, ["Deimos", "Ares", "Vanth"]); +// REMOVE_END + +// STEP_START incr +await client.set("total_crashes", 0); +const res7 = await client.incr("total_crashes"); +console.log(res7); // 1 +const res8 = await client.incrBy("total_crashes", 10); +console.log(res8); // 11 +// STEP_END + +// REMOVE_START +assert.equal(res7, 1); +assert.equal(res8, 11); + +await client.close(); +// REMOVE_END diff --git a/doctests/dt-tdigest.js b/doctests/dt-tdigest.js new file mode 100644 index 00000000000..c59168076e3 --- /dev/null +++ b/doctests/dt-tdigest.js @@ -0,0 +1,85 @@ +// EXAMPLE: tdigest_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START tdig_start +const res1 = await client.tDigest.create('bikes:sales', 100); +console.log(res1); // >>> OK + +const res2 = await client.tDigest.add('bikes:sales', [21]); +console.log(res2); // >>> OK + +const res3 = await client.tDigest.add('bikes:sales', [150, 95, 75, 34]); +console.log(res3); // >>> OK +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK') +assert.equal(res2, 'OK') +assert.equal(res3, 'OK') +// REMOVE_END + +// STEP_START tdig_cdf +const res4 = await client.tDigest.create('racer_ages'); +console.log(res4); // >>> OK + +const res5 = await client.tDigest.add('racer_ages', [ + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, 50.97, 25.41, 19.27, 85.71, 42.63 +]); +console.log(res5); // >>> OK + +const res6 = await client.tDigest.rank('racer_ages', [50]); +console.log(res6); // >>> [7] + +const res7 = await client.tDigest.rank('racer_ages', [50, 40]); +console.log(res7); // >>> [7, 4] +// STEP_END + +// REMOVE_START +assert.equal(res4, 'OK') +assert.equal(res5, 'OK') +assert.deepEqual(res6, [7]) +assert.deepEqual(res7, [7, 4]) +// REMOVE_END + +// STEP_START tdig_quant +const res8 = await client.tDigest.quantile('racer_ages', [0.5]); +console.log(res8); // >>> [44.2] + +const res9 = await client.tDigest.byRank('racer_ages', [4]); +console.log(res9); // >>> [42.63] +// STEP_END + +// STEP_START tdig_min +const res10 = await client.tDigest.min('racer_ages'); +console.log(res10); // >>> 19.27 + +const res11 = await client.tDigest.max('racer_ages'); +console.log(res11); // >>> 85.71 +// STEP_END + +// REMOVE_START +assert.deepEqual(res8, [44.2]) +assert.deepEqual(res9, [42.63]) +assert.equal(res10, 19.27) +assert.equal(res11, 85.71) +// REMOVE_END + +// STEP_START tdig_reset +const res12 = await client.tDigest.reset('racer_ages'); +console.log(res12); // >>> OK +// STEP_END + +// REMOVE_START +assert.equal(res12, 'OK') +await client.close(); +// REMOVE_END diff --git a/doctests/dt-topk.js b/doctests/dt-topk.js new file mode 100644 index 00000000000..49b929aed8a --- /dev/null +++ b/doctests/dt-topk.js @@ -0,0 +1,48 @@ +// EXAMPLE: topk_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.flushDb(); +// REMOVE_END + +// STEP_START topk +const res1 = await client.topK.reserve('bikes:keywords', 5, { + width: 2000, + depth: 7, + decay: 0.925 +}); +console.log(res1); // >>> OK + +const res2 = await client.topK.add('bikes:keywords', [ + 'store', + 'seat', + 'handlebars', + 'handles', + 'pedals', + 'tires', + 'store', + 'seat' +]); +console.log(res2); // >>> [null, null, null, null, null, 'handlebars', null, null] + +const res3 = await client.topK.list('bikes:keywords'); +console.log(res3); // >>> ['store', 'seat', 'pedals', 'tires', 'handles'] + +const res4 = await client.topK.query('bikes:keywords', ['store', 'handlebars']); +console.log(res4); // >>> [true, false] +// STEP_END + +// REMOVE_START +assert.equal(res1, 'OK') +assert.deepEqual(res2, [null, null, null, null, null, 'handlebars', null, null]) +assert.deepEqual(res3, ['store', 'seat', 'pedals', 'tires', 'handles']) +assert.deepEqual(res4, [1, 0]) +await client.close(); +// REMOVE_END + diff --git a/doctests/package-lock.json b/doctests/package-lock.json new file mode 100644 index 00000000000..2f30678e529 --- /dev/null +++ b/doctests/package-lock.json @@ -0,0 +1,889 @@ +{ + "name": "node-redis-doctests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-redis-doctests", + "version": "1.0.0", + "dependencies": { + "@xenova/transformers": "^2.17.2", + "redis": "file:/packages/redis" + } + }, + "..": { + "name": "redis-monorepo", + "extraneous": true, + "workspaces": [ + "./packages/client", + "./packages/test-utils", + "./packages/bloom", + "./packages/json", + "./packages/search", + "./packages/time-series", + "./packages/entraid", + "./packages/redis" + ], + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@release-it/bumper": "^7.0.5", + "@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": "^19.0.2", + "ts-node": "^10.9.2", + "tsx": "^4.7.0", + "typedoc": "^0.25.7", + "typescript": "^5.3.3" + } + }, + "../../../../../packages/redis": {}, + "node_modules/@huggingface/jinja": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", + "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@xenova/transformers": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz", + "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.2.2", + "onnxruntime-web": "1.14.0", + "sharp": "^0.32.0" + }, + "optionalDependencies": { + "onnxruntime-node": "1.14.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/flatbuffers": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", + "license": "SEE LICENSE IN LICENSE.txt" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onnx-proto": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", + "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==", + "license": "MIT", + "dependencies": { + "protobufjs": "^6.8.8" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz", + "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz", + "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==", + "license": "MIT", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "~1.14.0" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz", + "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^1.12.0", + "guid-typescript": "^1.0.9", + "long": "^4.0.0", + "onnx-proto": "^4.0.4", + "onnxruntime-common": "~1.14.0", + "platform": "^1.3.6" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redis": { + "resolved": "../../../../../packages/redis", + "link": true + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "ISC" + } + } +} diff --git a/doctests/package.json b/doctests/package.json new file mode 100644 index 00000000000..ee07eb44fb8 --- /dev/null +++ b/doctests/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-redis-doctests", + "version": "1.0.0", + "description": "Code examples for redis.io", + "main": "index.js", + "private": true, + "type": "module", + "dependencies": { + "redis": "file:/packages/redis", + "@xenova/transformers": "^2.17.2" + } +} diff --git a/doctests/query-agg.js b/doctests/query-agg.js new file mode 100644 index 00000000000..82d378b86b2 --- /dev/null +++ b/doctests/query-agg.js @@ -0,0 +1,139 @@ +// EXAMPLE: query_agg +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient } from 'redis'; +import { SCHEMA_FIELD_TYPE, FT_AGGREGATE_STEPS, FT_AGGREGATE_GROUP_BY_REDUCERS } from '@redis/search'; + +const client = createClient(); + +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}) + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START agg1 +const res1 = await client.ft.aggregate('idx:bicycle', '@condition:{new}', { + LOAD: ['__key', 'price'], + APPLY: { + expression: '@price - (@price * 0.1)', + AS: 'discounted' + } +}); + +console.log(res1.results.length); // >>> 5 +console.log(res1.results); // >>> +//[ +// [Object: null prototype] { __key: 'bicycle:0', price: '270' }, +// [Object: null prototype] { __key: 'bicycle:5', price: '810' }, +// [Object: null prototype] { __key: 'bicycle:6', price: '2300' }, +// [Object: null prototype] { __key: 'bicycle:7', price: '430' }, +// [Object: null prototype] { __key: 'bicycle:8', price: '1200' } +//] +// REMOVE_START +assert.strictEqual(res1.results.length, 5); +// REMOVE_END +// STEP_END + +// STEP_START agg2 +const res2 = await client.ft.aggregate('idx:bicycle', '*', { + LOAD: ['@price'], + STEPS: [{ + type: FT_AGGREGATE_STEPS.APPLY, + expression: '@price<1000', + AS: 'price_category' + },{ + type: FT_AGGREGATE_STEPS.GROUPBY, + properties: '@condition', + REDUCE:[{ + type: FT_AGGREGATE_GROUP_BY_REDUCERS.SUM, + property: '@price_category', + AS: 'num_affordable' + }] + }] +}); +console.log(res2.results.length); // >>> 3 +console.log(res2.results); // >>> +//[[Object: null prototype] { condition: 'refurbished', num_affordable: '1' }, +// [Object: null prototype] { condition: 'used', num_affordable: '1' }, +// [Object: null prototype] { condition: 'new', num_affordable: '3' } +//] +// REMOVE_START +assert.strictEqual(res2.results.length, 3); +// REMOVE_END +// STEP_END + +// STEP_START agg3 +const res3 = await client.ft.aggregate('idx:bicycle', '*', { + STEPS: [{ + type: FT_AGGREGATE_STEPS.APPLY, + expression: "'bicycle'", + AS: 'type' + }, { + type: FT_AGGREGATE_STEPS.GROUPBY, + properties: '@type', + REDUCE: [{ + type: FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT, + property: null, + AS: 'num_total' + }] + }] +}); +console.log(res3.results.length); // >>> 1 +console.log(res3.results); // >>> +//[ [Object: null prototype] { type: 'bicycle', num_total: '10' } ] +// REMOVE_START +assert.strictEqual(res3.results.length, 1); +// REMOVE_END +// STEP_END + +// STEP_START agg4 +const res4 = await client.ft.aggregate('idx:bicycle', '*', { + LOAD: ['__key'], + STEPS: [{ + type: FT_AGGREGATE_STEPS.GROUPBY, + properties: '@condition', + REDUCE: [{ + type: FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST, + property: '__key', + AS: 'bicycles' + }] + }] +}); +console.log(res4.results.length); // >>> 3 +console.log(res4.results); // >>> +//[[Object: null prototype] {condition: 'refurbished', bicycles: [ 'bicycle:9' ]}, +// [Object: null prototype] {condition: 'used', bicycles: [ 'bicycle:1', 'bicycle:2', 'bicycle:3', 'bicycle:4' ]}, +// [Object: null prototype] {condition: 'new', bicycles: [ 'bicycle:5', 'bicycle:6', 'bicycle:7', 'bicycle:0', 'bicycle:8' ]}] +// REMOVE_START +assert.strictEqual(res4.results.length, 3); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/query-combined.js b/doctests/query-combined.js new file mode 100644 index 00000000000..a2ad8d43c93 --- /dev/null +++ b/doctests/query-combined.js @@ -0,0 +1,191 @@ +// EXAMPLE: query_combined +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient } from 'redis'; +import { SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from '@redis/search'; +import { pipeline } from '@xenova/transformers'; + +function float32Buffer(arr) { + const floatArray = new Float32Array(arr); + const float32Buffer = Buffer.from(floatArray.buffer); + return float32Buffer; +} + +async function embedText(sentence) { + let modelName = 'Xenova/all-MiniLM-L6-v2'; + let pipe = await pipeline('feature-extraction', modelName); + + let vectorOutput = await pipe(sentence, { + pooling: 'mean', + normalize: true, + }); + + if (vectorOutput == null) { + throw new Error('vectorOutput is undefined'); + } + + const embedding = Object.values(vectorOutput.data); + + return embedding; +} + +let vector_query = float32Buffer(await embedText('That is a very happy person')); + +const client = createClient(); +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + }, + '$.description_embeddings': { + type: SCHEMA_FIELD_TYPE.VECTOR, + TYPE: 'FLOAT32', + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT, + DIM: 384, + DISTANCE_METRIC: 'COSINE', + AS: 'vector', + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}); + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_vector.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START combined1 +const res1 = await client.ft.search('idx:bicycle', '@price:[500 1000] @condition:{new}'); +console.log(res1.total); // >>> 1 +console.log(res1); // >>> +//{ +// total: 1, +// documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ] +//} +// REMOVE_START +assert.strictEqual(res1.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START combined2 +const res2 = await client.ft.search('idx:bicycle', 'kids @price:[500 1000] @condition:{used}'); +console.log(res2.total); // >>> 1 +console.log(res2); // >>> +// { +// total: 1, +// documents: [ { id: 'bicycle:2', value: [Object: null prototype] } ] +// } +// REMOVE_START +assert.strictEqual(res2.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START combined3 +const res3 = await client.ft.search('idx:bicycle', '(kids | small) @condition:{used}'); +console.log(res3.total); // >>> 2 +console.log(res3); // >>> +//{ +// total: 2, +// documents: [ +// { id: 'bicycle:2', value: [Object: null prototype] }, +// { id: 'bicycle:1', value: [Object: null prototype] } +// ] +//} +// REMOVE_START +assert.strictEqual(res3.total, 2); +// REMOVE_END +// STEP_END + +// STEP_START combined4 +const res4 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{used}'); +console.log(res4.total); // >>> 2 +console.log(res4); // >>> +//{ +// total: 2, +// documents: [ +// { id: 'bicycle:2', value: [Object: null prototype] }, +// { id: 'bicycle:1', value: [Object: null prototype] } +// ] +//} +// REMOVE_START +assert.strictEqual(res4.total, 2); +// REMOVE_END +// STEP_END + +// STEP_START combined5 +const res5 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{new | used}'); +console.log(res5.total); // >>> 3 +console.log(res5); // >>> +//{ +// total: 3, +// documents: [ +// { id: 'bicycle:1', value: [Object: null prototype] }, +// { id: 'bicycle:0', value: [Object: null prototype] }, +// { id: 'bicycle:2', value: [Object: null prototype] } +// ] +//} +// REMOVE_START +assert.strictEqual(res5.total, 3); +// REMOVE_END +// STEP_END + +// STEP_START combined6 +const res6 = await client.ft.search('idx:bicycle', '@price:[500 1000] -@condition:{new}'); +console.log(res6.total); // >>> 2 +console.log(res6); // >>> +//{ +// total: 2, +// documents: [ +// { id: 'bicycle:2', value: [Object: null prototype] }, +// { id: 'bicycle:9', value: [Object: null prototype] } +// ] +//} +// REMOVE_START +assert.strictEqual(res6.total, 2); +// REMOVE_END +// STEP_END + +// STEP_START combined7 +const res7 = await client.ft.search('idx:bicycle', + '(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector]', { + PARAMS: { query_vector: vector_query }, + DIALECT: 2 + } +); +console.log(res7.total); // >>> 2 +console.log(res7); // >>> +//{ +// total: 2, +// documents: [ +// { id: 'bicycle:2', value: [Object: null prototype] }, +// { id: 'bicycle:9', value: [Object: null prototype] } +// ] +//} +// REMOVE_START +assert.strictEqual(res7.total, 2); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/query-em.js b/doctests/query-em.js new file mode 100644 index 00000000000..9b5782e09ce --- /dev/null +++ b/doctests/query-em.js @@ -0,0 +1,121 @@ +// EXAMPLE: query_em +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient, SCHEMA_FIELD_TYPE } from 'redis'; + +const client = createClient(); + +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + }, + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}) + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START em1 +const res1 = await client.ft.search('idx:bicycle', '@price:[270 270]'); +console.log(res1.total); // >>> 1 +// REMOVE_START +assert.strictEqual(res1.total, 1); +// REMOVE_END + +try { + const res2 = await client.ft.search('idx:bicycle', '@price:[270]'); + console.log(res2.total); // >>> 1 + assert.strictEqual(res2.total, 1); +} catch (err) { + console.log("'@price:[270]' syntax not yet supported."); +} + +try { + const res3 = await client.ft.search('idx:bicycle', '@price==270'); + console.log(res3.total); // >>> 1 + assert.strictEqual(res3.total, 1); +} catch (err) { + console.log("'@price==270' syntax not yet supported."); +} + +// FILTER is not supported +// const res4 = await client.ft.search('idx:bicycle', '*', { +// FILTER: { +// field: 'price', +// min: 270, +// max: 270, +// } +// }); +// console.log(res4.total); // >>> 1 +// REMOVE_START +// assert.strictEqual(res4.total, 10); +// REMOVE_END +// STEP_END + +// STEP_START em2 +const res5 = await client.ft.search('idx:bicycle', '@condition:{new}'); +console.log(res5.total); // >>> 5 +// REMOVE_START +assert.strictEqual(res5.total, 5); +// REMOVE_END +// STEP_END + +// STEP_START em3 +await client.ft.create('idx:email', { + '$.email': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'email' + } +}, { + ON: 'JSON', + PREFIX: 'key:' +}) + +await client.json.set('key:1', '$', { email: 'test@redis.com' }); + +try { + const res6 = await client.ft.search('idx:email', 'test@redis.com', { DIALECT: 2 }); + console.log(res6); +} catch (err) { + console.log("'test@redis.com' syntax not yet supported."); +} +// REMOVE_START +await client.ft.dropIndex('idx:email', { DD: true }); +// REMOVE_END +// STEP_END + +// STEP_START em4 +const res7 = await client.ft.search('idx:bicycle', '@description:"rough terrain"'); +console.log(res7.total); // >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...) +// REMOVE_START +assert.strictEqual(res7.total, 1); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END diff --git a/doctests/query-ft.js b/doctests/query-ft.js new file mode 100644 index 00000000000..a70b2ea3f5d --- /dev/null +++ b/doctests/query-ft.js @@ -0,0 +1,84 @@ +// EXAMPLE: query_ft +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient, SCHEMA_FIELD_TYPE } from 'redis'; + +const client = createClient(); + +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.model': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'model' + }, + '$.brand': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'brand' + }, + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}) + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START ft1 +const res1 = await client.ft.search('idx:bicycle', '@description: kids'); +console.log(res1.total); // >>> 2 +// REMOVE_START +assert.strictEqual(res1.total, 2); +// REMOVE_END +// STEP_END + +// STEP_START ft2 +const res2 = await client.ft.search('idx:bicycle', '@model: ka*'); +console.log(res2.total); // >>> 1 +// REMOVE_START +assert.strictEqual(res2.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START ft3 +const res3 = await client.ft.search('idx:bicycle', '@brand: *bikes'); +console.log(res3.total); // >>> 2 +// REMOVE_START +assert.strictEqual(res3.total, 2); +// REMOVE_END +// STEP_END + +// STEP_START ft4 +const res4 = await client.ft.search('idx:bicycle', '%optamized%'); +console.log(res4); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]} +// REMOVE_START +assert.strictEqual(res4.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START ft5 +const res5 = await client.ft.search('idx:bicycle', '%%optamised%%'); +console.log(res5); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]} +// REMOVE_START +assert.strictEqual(res5.total, 1); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/query-geo.js b/doctests/query-geo.js new file mode 100644 index 00000000000..41ef2141eb8 --- /dev/null +++ b/doctests/query-geo.js @@ -0,0 +1,82 @@ +// EXAMPLE: query_geo +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient } from 'redis'; +import { SCHEMA_FIELD_TYPE } from '@redis/search'; + +const client = createClient(); + +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.store_location': { + type: SCHEMA_FIELD_TYPE.GEO, + AS: 'store_location' + }, + '$.pickup_zone': { + type: SCHEMA_FIELD_TYPE.GEOSHAPE, + AS: 'pickup_zone' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}) + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START geo1 +const res1= await client.ft.search('idx:bicycle', '@store_location:[-0.1778 51.5524 20 mi]'); +console.log(res1.total); // >>> 1 +console.log(res1); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]} +// REMOVE_START +assert.strictEqual(res1.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START geo2 +const params_dict_geo2 = { bike: 'POINT(-0.1278 51.5074)' }; +const q_geo2 = '@pickup_zone:[CONTAINS $bike]'; +const res2 = await client.ft.search('idx:bicycle', q_geo2, { PARAMS: params_dict_geo2, DIALECT: 3 }); +console.log(res2.total); // >>> 1 +console.log(res2); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]} +// REMOVE_START +assert.strictEqual(res2.total, 1); +// REMOVE_END +// STEP_END + +// STEP_START geo3 +const params_dict_geo3 = { europe: 'POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))' }; +const q_geo3 = '@pickup_zone:[WITHIN $europe]'; +const res3 = await client.ft.search('idx:bicycle', q_geo3, { PARAMS: params_dict_geo3, DIALECT: 3 }); +console.log(res3.total); // >>> 5 +console.log(res3); // >>> +// { +// total: 5, +// documents: [ +// { id: 'bicycle:5', value: [Object: null prototype] }, +// { id: 'bicycle:6', value: [Object: null prototype] }, +// { id: 'bicycle:7', value: [Object: null prototype] }, +// { id: 'bicycle:8', value: [Object: null prototype] }, +// { id: 'bicycle:9', value: [Object: null prototype] } +// ] +// } +// REMOVE_START +assert.strictEqual(res3.total, 5); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/query-range.js b/doctests/query-range.js new file mode 100644 index 00000000000..29b269e37b5 --- /dev/null +++ b/doctests/query-range.js @@ -0,0 +1,98 @@ +// EXAMPLE: query_range +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient, SCHEMA_FIELD_TYPE,} from 'redis'; + +const client = createClient(); + +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + }, + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}) + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_em.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START range1 +const res1 = await client.ft.search('idx:bicycle', '@price:[500 1000]'); +console.log(res1.total); // >>> 3 +// REMOVE_START +assert.strictEqual(res1.total, 3); +// REMOVE_END +// STEP_END + +// STEP_START range2 +// FILTER is not supported +// const res2 = await client.ft.search('idx:bicycle', '*', { +// FILTER: { +// field: 'price', +// min: 500, +// max: 1000, +// } +// }); +// console.log(res2.total); // >>> 3 +// REMOVE_START +// assert.strictEqual(res2.total, 3); +// REMOVE_END +// STEP_END + +// STEP_START range3 +// FILTER is not supported +// const res3 = await client.ft.search('idx:bicycle', '*', { +// FILTER: { +// field: 'price', +// min: '(1000', +// max: '+inf, +// } +// }); +// console.log(res3.total); // >>> 5 +// REMOVE_START +// assert.strictEqual(res3.total, 5); +// REMOVE_END +// STEP_END + +// STEP_START range4 +const res4 = await client.ft.search( + 'idx:bicycle', + '@price:[-inf 2000]', + { + SORTBY: 'price', + LIMIT: { from: 0, size: 5 } + } +); +console.log(res4.total); // >>> 7 +console.log(res4); // >>> { total: 7, documents: [ { id: 'bicycle:0', value: [Object: null prototype] }, { id: 'bicycle:7', value: [Object: null prototype] }, { id: 'bicycle:5', value: [Object: null prototype] }, { id: 'bicycle:2', value: [Object: null prototype] }, { id: 'bicycle:9', value: [Object: null prototype] } ] } +// REMOVE_START +assert.strictEqual(res4.total, 7); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/query-vector.js b/doctests/query-vector.js new file mode 100644 index 00000000000..91ee63120d3 --- /dev/null +++ b/doctests/query-vector.js @@ -0,0 +1,110 @@ +// EXAMPLE: query_vector +// HIDE_START +import assert from 'node:assert'; +import fs from 'node:fs'; +import { createClient } from 'redis'; +import { SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from '@redis/search'; +import { pipeline } from '@xenova/transformers'; + +function float32Buffer(arr) { + const floatArray = new Float32Array(arr); + const float32Buffer = Buffer.from(floatArray.buffer); + return float32Buffer; +} + +async function embedText(sentence) { + let modelName = 'Xenova/all-MiniLM-L6-v2'; + let pipe = await pipeline('feature-extraction', modelName); + + let vectorOutput = await pipe(sentence, { + pooling: 'mean', + normalize: true, + }); + + const embedding = Object.values(vectorOutput?.data); + + return embedding; +} + +const vector_query = float32Buffer(await embedText('That is a very happy person')); + +const client = createClient(); +await client.connect().catch(console.error); + +// create index +await client.ft.create('idx:bicycle', { + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.description_embeddings': { + type: SCHEMA_FIELD_TYPE.VECTOR, + TYPE: 'FLOAT32', + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT, + DIM: 384, + DISTANCE_METRIC: 'COSINE', + AS: 'vector' + } +}, { + ON: 'JSON', + PREFIX: 'bicycle:' +}); + +// load data +const bicycles = JSON.parse(fs.readFileSync('data/query_vector.json', 'utf8')); + +await Promise.all( + bicycles.map((bicycle, bid) => { + return client.json.set(`bicycle:${bid}`, '$', bicycle); + }) +); +// HIDE_END + +// STEP_START vector1 +const res1 = await client.ft.search('idx:bicycle', + '*=>[KNN 3 @vector $query_vector AS score]', { + PARAMS: { query_vector: vector_query }, + RETURN: ['description'], + DIALECT: 2 + } +); +console.log(res1.total); // >>> 3 +console.log(res1); // >>> +//{ +// total: 3, +// documents: [ +// { id: 'bicycle:0', value: [Object: null prototype] {} }, +// { id: 'bicycle:2', value: [Object: null prototype] {} }, +// { id: 'bicycle:9', value: [Object: null prototype] {} } +// ] +//} +// REMOVE_START +assert.strictEqual(res1.total, 3); +// REMOVE_END +// STEP_END + +// STEP_START vector2 +const res2 = await client.ft.search('idx:bicycle', + '@vector:[VECTOR_RANGE 0.9 $query_vector]=>{$YIELD_DISTANCE_AS: vector_dist}', { + PARAMS: { query_vector: vector_query }, + SORTBY: 'vector_dist', + RETURN: ['vector_dist', 'description'], + DIALECT: 2 + } +); +console.log(res2.total); // >>> 1 +console.log(res2); // >>> +//{ +// total: 1, +// documents: [ { id: 'bicycle:0', value: [Object: null prototype] } ] +//} +// REMOVE_START +assert.strictEqual(res2.total, 1); +// REMOVE_END +// STEP_END + +// REMOVE_START +// destroy index and data +await client.ft.dropIndex('idx:bicycle', { DD: true }); +await client.close(); +// REMOVE_END \ No newline at end of file diff --git a/doctests/run_examples.sh b/doctests/run_examples.sh new file mode 100755 index 00000000000..ee7b50dc69a --- /dev/null +++ b/doctests/run_examples.sh @@ -0,0 +1,15 @@ +#!/bin/sh + + +basepath=`readlink -f $1` +if [ $? -ne 0 ]; then +basepath=`readlink -f $(dirname $0)` +fi +echo "No path specified, using ${basepath}" + +set -e +cd ${basepath} +for i in `ls ${basepath}/*.js`; do + redis-cli flushdb + node $i +done \ No newline at end of file diff --git a/doctests/search-quickstart.js b/doctests/search-quickstart.js new file mode 100644 index 00000000000..ec3f65a5e3a --- /dev/null +++ b/doctests/search-quickstart.js @@ -0,0 +1,231 @@ +// EXAMPLE: search_quickstart +// REMOVE_START +import assert from 'assert'; +// REMOVE_END +// HIDE_START +import { createClient, SCHEMA_FIELD_TYPE } from 'redis'; +// HIDE_END +// STEP_START connect +const client = createClient(); +client.on('error', err => console.log('Redis Client Error', err)); + +await client.connect(); +// STEP_END + +// STEP_START data_sample +const bicycle1 = { + brand: 'Velorim', + model: 'Jigger', + price: 270, + description: + 'Small and powerful, the Jigger is the best ' + + 'ride for the smallest of tikes! This is the tiniest kids\u2019 ' + + 'pedal bike on the market available without a coaster brake, the ' + + 'Jigger is the vehicle of choice for the rare tenacious little' + + 'rider raring to go.', + condition: 'new' +}; +// STEP_END +const bicycles = [ + bicycle1, + { + brand: 'Bicyk', + model: 'Hillcraft', + price: 1200, + description: 'Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!', + condition: 'used' + }, + { + brand: 'Nord', + model: 'Chook air 5', + price: 815, + description: 'The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.', + condition: 'used' + }, + { + brand: 'Eva', + model: 'Eva 291', + price: 3400, + description: 'The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It\u2019s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!', + condition: 'used' + }, + { + brand: 'Noka Bikes', + model: 'Kahuna', + price: 3200, + description: 'Whether you want to try your hand at XC racing or are looking for a lively trail bike that\'s just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women\u2019s saddle, different bars and unique colourway.', + condition: 'used' + }, + { + brand: 'Breakout', + model: 'XBN 2.1 Alloy', + price: 810, + description: 'The XBN 2.1 Alloy is our entry-level road bike \u2013 but that\u2019s not to say that it\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.', + condition: 'new' + }, + { + brand: 'ScramBikes', + model: 'WattBike', + price: 2300, + description: 'The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\u2019s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.', + condition: 'new' + }, + { + brand: 'Peaknetic', + model: 'Secto', + price: 430, + description: 'If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.', + condition: 'new' + }, + { + brand: 'nHill', + model: 'Summit', + price: 1200, + description: 'This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.', + condition: 'new' + }, + { + model: 'ThrillCycle', + brand: 'BikeShind', + price: 815, + description: 'An artsy, retro-inspired bicycle that\u2019s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\u2019t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.', + condition: 'refurbished' + } +]; +// STEP_START create_index +const schema = { + '$.brand': { + type: SCHEMA_FIELD_TYPE.TEXT, + SORTABLE: true, + AS: 'brand' + }, + '$.model': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'model' + }, + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + }, + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + } +}; + +try { + await client.ft.create('idx:bicycle', schema, { + ON: 'JSON', + PREFIX: 'bicycle:' + }); +} catch (e) { + if (e.message === 'Index already exists') { + console.log('Index exists already, skipped creation.'); + } else { + // Something went wrong, perhaps RediSearch isn't installed... + console.error(e); + process.exit(1); + } +} +// STEP_END + +// STEP_START add_documents +await Promise.all( + bicycles.map((bicycle, i) => client.json.set(`bicycle:${i}`, '$', bicycle)) +); +// STEP_END + +// STEP_START wildcard_query +let result = await client.ft.search('idx:bicycle', '*', { + LIMIT: { + from: 0, + size: 10 + } +}); + +console.log(JSON.stringify(result, null, 2)); + +/* +{ + "total": 10, + "documents": ... +} +*/ +// STEP_END + +// REMOVE_START +assert.equal(result.documents[0].id, 'bicycle:0'); +// REMOVE_END + +// STEP_START query_single_term +result = await client.ft.search( + 'idx:bicycle', + '@model:Jigger', + { + LIMIT: { + from: 0, + size: 10 + } +}); + +console.log(JSON.stringify(result, null, 2)); +/* +{ + "total": 1, + "documents": [{ + "id": "bicycle:0", + "value": { + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new" + } + }] +} + */ +// STEP_END +// REMOVE_START +assert.equal(result.documents[0].id, 'bicycle:0'); +// REMOVE_END + +// STEP_START query_exact_matching +result = await client.ft.search( + 'idx:bicycle', + '@brand:"Noka Bikes"', + { + LIMIT: { + from: 0, + size: 10 + } + } +); + +console.log(JSON.stringify(result, null, 2)); + +/* +{ + "total": 1, + "documents": [{ + "id": "bicycle:4", + "value": { + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.", + "condition": "used" + } + }] +} +*/ +// STEP_END + +// REMOVE_START +assert.equal(result.documents[0].id, 'bicycle:4'); +// REMOVE END + +await client.close(); diff --git a/doctests/string-set-get-example.js b/doctests/string-set-get-example.js new file mode 100644 index 00000000000..b9be382285e --- /dev/null +++ b/doctests/string-set-get-example.js @@ -0,0 +1,27 @@ +// EXAMPLE: set_and_get +// REMOVE_START +import assert from "node:assert"; +// REMOVE_END + +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', err => console.log('Redis Client Error', err)); + +await client.connect().catch(console.error); + +// HIDE_END +await client.set('bike:1', 'Process 134'); +const value = await client.get('bike:1'); +console.log(value); +// returns 'Process 134' +//REMOVE_START +assert.equal(value, 'Process 134'); +await client.del('bike:1'); +//REMOVE_END + +// HIDE_START +await client.close(); +// HIDE_END diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 41991a24172..173e8959afa 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -3,7 +3,7 @@ import { RedisArgument, CommandArguments, BlobStringReply, ArrayReply, Command } /** * Common options for SCAN-type commands - * + * * @property MATCH - Pattern to filter returned keys * @property COUNT - Hint for how many elements to return per iteration */ @@ -14,7 +14,7 @@ export interface ScanCommonOptions { /** * Parses scan arguments for SCAN-type commands - * + * * @param parser - The command parser * @param cursor - The cursor position for iteration * @param options - Scan options @@ -36,7 +36,7 @@ export function parseScanArguments( /** * Pushes scan arguments to the command arguments array - * + * * @param args - The command arguments array * @param cursor - The cursor position for iteration * @param options - Scan options @@ -62,7 +62,7 @@ export function pushScanArguments( /** * Options for the SCAN command - * + * * @property TYPE - Filter by value type */ export interface ScanOptions extends ScanCommonOptions { @@ -74,7 +74,7 @@ export default { IS_READ_ONLY: true, /** * Constructs the SCAN command - * + * * @param parser - The command parser * @param cursor - The cursor position to start scanning from * @param options - Scan options @@ -87,14 +87,16 @@ export default { if (options?.TYPE) { parser.push('TYPE', options.TYPE); } + console.log('eeeeeeeeee', parser.redisArgs) }, /** * Transforms the SCAN reply into a structured object - * + * * @param reply - The raw reply containing cursor and keys * @returns Object with cursor and keys properties */ transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { + console.log(cursor, keys) return { cursor, keys From 4ae14bb558946258b075baf31e91b8d28329acd2 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Wed, 23 Jul 2025 18:21:13 +0300 Subject: [PATCH 191/244] Add Redis transparent proxy test utilities (#3019) --- packages/client/lib/test-utils.ts | 6 + packages/test-utils/lib/index.ts | 38 +++ packages/test-utils/lib/redis-proxy-spec.ts | 111 +++++++ packages/test-utils/lib/redis-proxy.ts | 329 ++++++++++++++++++++ packages/test-utils/lib/test-utils.ts | 25 ++ 5 files changed, 509 insertions(+) create mode 100644 packages/test-utils/lib/redis-proxy-spec.ts create mode 100644 packages/test-utils/lib/redis-proxy.ts create mode 100644 packages/test-utils/lib/test-utils.ts diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 809ee788e92..b9b906e943c 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -93,6 +93,12 @@ export const GLOBAL = { password: 'password' } }, + OPEN_RESP_3: { + serverArguments: [...DEBUG_MODE_ARGS], + clientOptions: { + RESP: 3, + } + }, ASYNC_BASIC_AUTH: { serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], clientOptions: { diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index aab1c700f5e..64b9abc7f48 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -26,6 +26,7 @@ import { hideBin } from 'yargs/helpers'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { RedisProxy, getFreePortNumber } from './redis-proxy'; interface TestUtilsConfig { /** @@ -296,7 +297,44 @@ export default class TestUtils { } }); } + testWithProxiedClient( + title: string, + fn: (proxiedClient: RedisClientType, proxy: RedisProxy) => unknown, + options: ClientTestOptions + ) { + + this.testWithClient(title, async (client) => { + const freePort = await getFreePortNumber() + const socketOptions = client?.options?.socket; + const proxy = new RedisProxy({ + listenHost: '127.0.0.1', + listenPort: freePort, + //@ts-ignore + targetPort: socketOptions.port, + //@ts-ignore + targetHost: socketOptions.host, + enableLogging: true + }); + + + await proxy.start(); + const proxyClient = client.duplicate({ + socket: { + port: proxy.config.listenPort, + host: proxy.config.listenHost + }, + }); + await proxyClient.connect(); + + try { + await fn(proxyClient, proxy); + } finally { + await proxyClient.destroy(); + await proxy.stop() + } + }, options); + } testWithClientSentinel< M extends RedisModules = {}, F extends RedisFunctions = {}, diff --git a/packages/test-utils/lib/redis-proxy-spec.ts b/packages/test-utils/lib/redis-proxy-spec.ts new file mode 100644 index 00000000000..89b3b28c35a --- /dev/null +++ b/packages/test-utils/lib/redis-proxy-spec.ts @@ -0,0 +1,111 @@ +import { strict as assert } from 'node:assert'; +import { Buffer } from 'node:buffer'; +import { testUtils, GLOBAL } from './test-utils'; +import { RedisProxy } from './redis-proxy'; +import type { RedisClientType } from '@redis/client/lib/client/index.js'; + +describe('RedisSocketProxy', function () { + testUtils.testWithClient('basic proxy functionality', async (client: RedisClientType) => { + const socketOptions = client?.options?.socket; + //@ts-ignore + assert(socketOptions?.port, 'Test requires a TCP connection to Redis'); + + const proxyPort = 50000 + Math.floor(Math.random() * 10000); + const proxy = new RedisProxy({ + listenHost: '127.0.0.1', + listenPort: proxyPort, + //@ts-ignore + targetPort: socketOptions.port, + //@ts-ignore + targetHost: socketOptions.host || '127.0.0.1', + enableLogging: true + }); + + const proxyEvents = { + connections: [] as any[], + dataTransfers: [] as any[] + }; + + proxy.on('connection', (connectionInfo) => { + proxyEvents.connections.push(connectionInfo); + }); + + proxy.on('data', (connectionId, direction, data) => { + proxyEvents.dataTransfers.push({ connectionId, direction, dataLength: data.length }); + }); + + try { + await proxy.start(); + + const proxyClient = client.duplicate({ + socket: { + port: proxyPort, + host: '127.0.0.1' + }, + }); + + await proxyClient.connect(); + + const stats = proxy.getStats(); + assert.equal(stats.activeConnections, 1, 'Should have one active connection'); + assert.equal(proxyEvents.connections.length, 1, 'Should have recorded one connection event'); + + const pingResult = await proxyClient.ping(); + assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy'); + + const clientToServerTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'client->server'); + const serverToClientTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'server->client'); + + assert(clientToServerTransfers.length > 0, 'Should have client->server data transfers'); + assert(serverToClientTransfers.length > 0, 'Should have server->client data transfers'); + + const testKey = `test:proxy:${Date.now()}`; + const testValue = 'proxy-test-value'; + + await proxyClient.set(testKey, testValue); + const retrievedValue = await proxyClient.get(testKey); + assert.equal(retrievedValue, testValue, 'Should be able to set and get values through proxy'); + + proxyClient.destroy(); + + + } finally { + await proxy.stop(); + } + }, GLOBAL.SERVERS.OPEN_RESP_3); + + testUtils.testWithProxiedClient('custom message injection via proxy client', + async (proxiedClient: RedisClientType, proxy: RedisProxy) => { + const customMessageTransfers: any[] = []; + + proxy.on('data', (connectionId, direction, data) => { + if (direction === 'server->client') { + customMessageTransfers.push({ connectionId, dataLength: data.length, data }); + } + }); + + + const stats = proxy.getStats(); + assert.equal(stats.activeConnections, 1, 'Should have one active connection'); + + // Send a resp3 push + const customMessage = Buffer.from('>4\r\n$6\r\nMOVING\r\n:1\r\n:2\r\n$6\r\nhost:3\r\n'); + + const sendResults = proxy.sendToAllClients(customMessage); + assert.equal(sendResults.length, 1, 'Should send to one client'); + assert.equal(sendResults[0].success, true, 'Custom message send should succeed'); + + + const customMessageFound = customMessageTransfers.find(transfer => + transfer.dataLength === customMessage.length + ); + assert(customMessageFound, 'Should have recorded the custom message transfer'); + + assert.equal(customMessageFound.dataLength, customMessage.length, + 'Custom message length should match'); + + const pingResult = await proxiedClient.ping(); + assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy'); + + }, GLOBAL.SERVERS.OPEN_RESP_3) +}); diff --git a/packages/test-utils/lib/redis-proxy.ts b/packages/test-utils/lib/redis-proxy.ts new file mode 100644 index 00000000000..217ec528a33 --- /dev/null +++ b/packages/test-utils/lib/redis-proxy.ts @@ -0,0 +1,329 @@ +import * as net from 'net'; +import { EventEmitter } from 'events'; + +interface ProxyConfig { + readonly listenPort: number; + readonly listenHost?: string; + readonly targetHost: string; + readonly targetPort: number; + readonly timeout?: number; + readonly enableLogging?: boolean; +} + +interface ConnectionInfo { + readonly id: string; + readonly clientAddress: string; + readonly clientPort: number; + readonly connectedAt: Date; +} + +interface ActiveConnection extends ConnectionInfo { + readonly clientSocket: net.Socket; + readonly serverSocket: net.Socket; +} + +type SendResult = + | { readonly success: true; readonly connectionId: string } + | { readonly success: false; readonly error: string; readonly connectionId: string }; + +type DataDirection = 'client->server' | 'server->client'; + +interface ProxyStats { + readonly activeConnections: number; + readonly totalConnections: number; + readonly connections: readonly ConnectionInfo[]; +} + +interface ProxyEvents { + /** Emitted when a new client connects */ + 'connection': (connectionInfo: ConnectionInfo) => void; + /** Emitted when a connection is closed */ + 'disconnect': (connectionInfo: ConnectionInfo) => void; + /** Emitted when data is transferred */ + 'data': (connectionId: string, direction: DataDirection, data: Buffer) => void; + /** Emitted when an error occurs */ + 'error': (error: Error, connectionId?: string) => void; + /** Emitted when the proxy server starts */ + 'listening': (host: string, port: number) => void; + /** Emitted when the proxy server stops */ + 'close': () => void; +} + +export class RedisProxy extends EventEmitter { + private readonly server: net.Server; + public readonly config: Required; + private readonly connections: Map; + private isRunning: boolean; + + constructor(config: ProxyConfig) { + super(); + + + this.config = { + listenHost: '127.0.0.1', + timeout: 30000, + enableLogging: false, + ...config + }; + + this.connections = new Map(); + this.isRunning = false; + this.server = this.createServer(); + } + + public async start(): Promise { + return new Promise((resolve, reject) => { + if (this.isRunning) { + reject(new Error('Proxy is already running')); + return; + } + + this.server.listen(this.config.listenPort, this.config.listenHost, () => { + this.isRunning = true; + this.log(`Proxy listening on ${this.config.listenHost}:${this.config.listenPort}`); + this.log(`Forwarding to Redis server at ${this.config.targetHost}:${this.config.targetPort}`); + this.emit('listening', this.config.listenHost, this.config.listenPort); + resolve(); + }); + + this.server.on('error', (error) => { + this.emit('error', error); + reject(error); + }); + }); + } + + public async stop(): Promise { + return new Promise((resolve) => { + if (!this.isRunning) { + resolve(); + return; + } + + Array.from(this.connections.keys()).forEach((connectionId) => { + this.closeConnection(connectionId); + }); + + this.server.close(() => { + this.isRunning = false; + this.log('Proxy server stopped'); + this.emit('close'); + resolve(); + }); + }); + } + + public getStats(): ProxyStats { + const connections = Array.from(this.connections.values()); + + return { + activeConnections: connections.length, + totalConnections: connections.length, + connections: connections.map((conn) => ({ + id: conn.id, + clientAddress: conn.clientAddress, + clientPort: conn.clientPort, + connectedAt: conn.connectedAt, + })) + }; + } + + public closeConnection(connectionId: string): boolean { + const connection = this.connections.get(connectionId); + if (!connection) { + return false; + } + + connection.clientSocket.destroy(); + connection.serverSocket.destroy(); + this.connections.delete(connectionId); + this.emit('disconnect', connection); + return true; + } + + public sendToClient(connectionId: string, data: Buffer): SendResult { + const connection = this.connections.get(connectionId); + if (!connection) { + return { + success: false, + error: 'Connection not found', + connectionId + }; + } + + if (connection.clientSocket.destroyed || !connection.clientSocket.writable) { + return { + success: false, + error: 'Client socket is not writable', + connectionId + }; + } + + try { + connection.clientSocket.write(data); + + this.log(`Sent ${data.length} bytes to client ${connectionId}`); + this.emit('data', connectionId, 'server->client', data); + + return { + success: true, + connectionId + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + this.log(`Failed to send data to client ${connectionId}: ${errorMessage}`); + return { + success: false, + error: errorMessage, + connectionId + }; + } + } + + public sendToAllClients(data: Buffer): readonly SendResult[] { + const connectionIds = Array.from(this.connections.keys()); + const results = connectionIds.map((connectionId) => + this.sendToClient(connectionId, data) + ); + + const successCount = results.filter((result) => result.success).length; + const totalCount = results.length; + + this.log(`Sent ${data.length} bytes to ${successCount}/${totalCount} clients`); + + return results; + } + + public sendToClients(connectionIds: readonly string[], data: Buffer): readonly SendResult[] { + const results = connectionIds.map((connectionId) => + this.sendToClient(connectionId, data) + ); + + const successCount = results.filter((result) => result.success).length; + const totalCount = results.length; + + this.log(`Sent ${data.length} bytes to ${successCount}/${totalCount} specified clients`); + + return results; + } + + public getActiveConnectionIds(): readonly string[] { + return Array.from(this.connections.keys()); + } + + private createServer(): net.Server { + return net.createServer((clientSocket) => { + this.handleClientConnection(clientSocket); + }); + } + + private handleClientConnection(clientSocket: net.Socket): void { + const connectionId = this.generateConnectionId(); + const serverSocket = net.createConnection({ + host: this.config.targetHost, + port: this.config.targetPort + }); + + const connectionInfo: ActiveConnection = { + id: connectionId, + clientAddress: clientSocket.remoteAddress || 'unknown', + clientPort: clientSocket.remotePort || 0, + connectedAt: new Date(), + clientSocket, + serverSocket + }; + + this.connections.set(connectionId, connectionInfo); + this.log(`New connection ${connectionId} from ${connectionInfo.clientAddress}:${connectionInfo.clientPort}`); + + clientSocket.setTimeout(this.config.timeout); + + serverSocket.on('connect', () => { + this.log(`Connected to Redis server for connection ${connectionId}`); + this.emit('connection', connectionInfo); + }); + + clientSocket.on('data', (data) => { + this.emit('data', connectionId, 'client->server', data); + serverSocket.write(data); + }); + + serverSocket.on('data', (data) => { + this.emit('data', connectionId, 'server->client', data); + clientSocket.write(data); + }); + + clientSocket.on('close', () => { + this.log(`Client disconnected for connection ${connectionId}`); + serverSocket.destroy(); + this.cleanupConnection(connectionId); + }); + + serverSocket.on('close', () => { + this.log(`Server disconnected for connection ${connectionId}`); + clientSocket.destroy(); + this.cleanupConnection(connectionId); + }); + + clientSocket.on('error', (error) => { + this.log(`Client error for connection ${connectionId}: ${error.message}`); + this.emit('error', error, connectionId); + serverSocket.destroy(); + this.cleanupConnection(connectionId); + }); + + serverSocket.on('error', (error) => { + this.log(`Server error for connection ${connectionId}: ${error.message}`); + this.emit('error', error, connectionId); + clientSocket.destroy(); + this.cleanupConnection(connectionId); + }); + + clientSocket.on('timeout', () => { + this.log(`Connection ${connectionId} timed out`); + clientSocket.destroy(); + serverSocket.destroy(); + this.cleanupConnection(connectionId); + }); + } + + private cleanupConnection(connectionId: string): void { + const connection = this.connections.get(connectionId); + if (connection) { + this.connections.delete(connectionId); + this.emit('disconnect', connection); + } + } + + private generateConnectionId(): string { + return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + private log(message: string): void { + if (this.config.enableLogging) { + console.log(`[RedisProxy] ${new Date().toISOString()} - ${message}`); + } + } +} +import { createServer } from 'net'; + +export function getFreePortNumber(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + + server.listen(0, () => { + const address = server.address(); + server.close(() => { + if (address && typeof address === 'object') { + resolve(address.port); + } + }); + }); + + server.on('error', reject); + }); +} + +export { RedisProxy as RedisTransparentProxy }; +export type { ProxyConfig, ConnectionInfo, ProxyEvents, SendResult, DataDirection, ProxyStats }; + diff --git a/packages/test-utils/lib/test-utils.ts b/packages/test-utils/lib/test-utils.ts new file mode 100644 index 00000000000..fe27bd93d37 --- /dev/null +++ b/packages/test-utils/lib/test-utils.ts @@ -0,0 +1,25 @@ +import TestUtils from './index' + +export const testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.2-M01-pre' +}); + + + +export const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? + ['--enable-debug-command', 'yes'] : + []; + +export const GLOBAL = { + SERVERS: { + + OPEN_RESP_3: { + serverArguments: [...DEBUG_MODE_ARGS], + clientOptions: { + RESP: 3, + } + }, + } +} From 1c266371a3dd1d4fa66dfbe52892eec5fc77af6f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:52:42 +0000 Subject: [PATCH 192/244] Release client@5.6.1 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8ff84d8259..f5ff9b6b95c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", + "integrity": "sha512-wmP9kCFElCSr4MM4+1E4VckDuN4wLtiXSM/J0rKVQppajxQhowci89RGZr2OdLualowb8SRJ/R6OjsXrn9ZNFA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.0", diff --git a/packages/client/package.json b/packages/client/package.json index ee98d77ca1f..9fe67df521e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 945b77af16ae4195856706ab464475a977d5c59a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:52:49 +0000 Subject: [PATCH 193/244] Release bloom@5.6.1 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5ff9b6b95c..53202b80163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.0.tgz", + "integrity": "sha512-l13/d6BaZDJzogzZJEphIeZ8J0hpQpjkMiozomTm6nJiMNYkoPsNOBOOQua4QsG0fFjyPmLMDJFPAp5FBQtTXg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 47d1f6978ef..ef900b00a6d 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" }, "devDependencies": { "@redis/test-utils": "*" From fc5ff18875bca9c6f8b379637f74d3916edef97f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:52:56 +0000 Subject: [PATCH 194/244] Release json@5.6.1 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53202b80163..35177cf51a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.0.tgz", + "integrity": "sha512-YQN9ZqaSDpdLfJqwzcF4WeuJMGru/h4WsV7GeeNtXsSeyQjHTyDxrd48xXfRRJGv7HitA7zGnzdHplNeKOgrZA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.0", diff --git a/packages/json/package.json b/packages/json/package.json index edfdaec8efa..e62377feb14 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" }, "devDependencies": { "@redis/test-utils": "*" From 726e2c7c649110a307dd3553c0f6c16dc0324aea Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:53:04 +0000 Subject: [PATCH 195/244] Release search@5.6.1 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35177cf51a2..b02d5b9dd7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.6.0" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.0.tgz", + "integrity": "sha512-sLgQl92EyMVNHtri5K8Q0j2xt9c0cO9HYurXz667Un4xeUYR+B/Dw5lLG35yqO7VvVxb9amHJo9sAWumkKZYwA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index c876d7460c3..c8fb49d5332 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" }, "devDependencies": { "@redis/test-utils": "*" From 3c4defda0ba9eb66b300321dcafeff85c7c31a8c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:53:11 +0000 Subject: [PATCH 196/244] Release time-series@5.6.1 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b02d5b9dd7c..2acb520b064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.6.0" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.0.tgz", + "integrity": "sha512-tXABmN1vu4aTNL3WI4Iolpvx/5jgil2Bs31ozvKblT+jkUoRkk8ykmYo9Pv/Mp7Gk6/Qkr/2rMgVminrt/4BBQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.1", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 762aea77661..1c42757523f 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" }, "devDependencies": { "@redis/test-utils": "*" From 3365920470b2297c01580901c9e82ff452b2f5ec Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:53:19 +0000 Subject: [PATCH 197/244] Release entraid@5.6.1 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2acb520b064..073126960af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 173bd0a6c67..4e702fabb65 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.6.0" + "@redis/client": "^5.6.1" }, "devDependencies": { "@types/express": "^4.17.21", From e96db0d13c8161fa81d27b63072c09849bcbb6a2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jul 2025 15:53:25 +0000 Subject: [PATCH 198/244] Release redis@5.6.1 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 073126960af..71e0ce79df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.6.0", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.6.0", - "@redis/client": "5.6.0", - "@redis/json": "5.6.0", - "@redis/search": "5.6.0", - "@redis/time-series": "5.6.0" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.0.tgz", - "integrity": "sha512-l13/d6BaZDJzogzZJEphIeZ8J0hpQpjkMiozomTm6nJiMNYkoPsNOBOOQua4QsG0fFjyPmLMDJFPAp5FBQtTXg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.0" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", - "integrity": "sha512-wmP9kCFElCSr4MM4+1E4VckDuN4wLtiXSM/J0rKVQppajxQhowci89RGZr2OdLualowb8SRJ/R6OjsXrn9ZNFA==", + "version": "5.6.1", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.6.1", + "@redis/client": "5.6.1", + "@redis/json": "5.6.1", + "@redis/search": "5.6.1", + "@redis/time-series": "5.6.1" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.0.tgz", - "integrity": "sha512-YQN9ZqaSDpdLfJqwzcF4WeuJMGru/h4WsV7GeeNtXsSeyQjHTyDxrd48xXfRRJGv7HitA7zGnzdHplNeKOgrZA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.0" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.0.tgz", - "integrity": "sha512-sLgQl92EyMVNHtri5K8Q0j2xt9c0cO9HYurXz667Un4xeUYR+B/Dw5lLG35yqO7VvVxb9amHJo9sAWumkKZYwA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.0" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.0.tgz", - "integrity": "sha512-tXABmN1vu4aTNL3WI4Iolpvx/5jgil2Bs31ozvKblT+jkUoRkk8ykmYo9Pv/Mp7Gk6/Qkr/2rMgVminrt/4BBQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.0" - } - }, "packages/search": { "name": "@redis/search", "version": "5.6.1", diff --git a/packages/redis/package.json b/packages/redis/package.json index 32251c7e501..bdb86663480 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.6.0", + "version": "5.6.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.6.0", - "@redis/client": "5.6.0", - "@redis/json": "5.6.0", - "@redis/search": "5.6.0", - "@redis/time-series": "5.6.0" + "@redis/bloom": "5.6.1", + "@redis/client": "5.6.1", + "@redis/json": "5.6.1", + "@redis/search": "5.6.1", + "@redis/time-series": "5.6.1" }, "engines": { "node": ">= 18" From ff8319d2d7fe7afb6ae1eb129ff5526370aef4af Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:39:32 +0300 Subject: [PATCH 199/244] fix(pool): chain promise handlers to prevent unhandled rejections (#3035) --- packages/client/lib/client/pool.spec.ts | 31 +++++++++++++++++++++++++ packages/client/lib/client/pool.ts | 5 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/client/pool.spec.ts b/packages/client/lib/client/pool.spec.ts index 8fc7a258df9..f292dc171c7 100644 --- a/packages/client/lib/client/pool.spec.ts +++ b/packages/client/lib/client/pool.spec.ts @@ -8,4 +8,35 @@ describe('RedisClientPool', () => { 'PONG' ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientPool( + 'proper error propagation in sequential operations', + async (pool) => { + let hasUnhandledRejection = false; + + process.once('unhandledRejection', () => { + hasUnhandledRejection = true; + }); + + const groupName = 'test-group'; + const streamName = 'test-stream'; + + // First attempt - should succeed + await pool.xGroupCreate(streamName, groupName, '0', { + MKSTREAM: true, + }); + + // Subsequent attempts - should all throw BUSYGROUP errors and be handled properly + for (let i = 0; i < 3; i++) { + await assert.rejects( + pool.xGroupCreate(streamName, groupName, '0', { + MKSTREAM: true, + }) + ); + } + + assert.equal(hasUnhandledRejection, false); + }, + GLOBAL.SERVERS.OPEN + ); }); diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index 6f633c9caa7..b53bb2c7e61 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -438,8 +438,9 @@ export class RedisClientPool< ) { const result = fn(node.value); if (result instanceof Promise) { - result.then(resolve, reject); - result.finally(() => this.#returnClient(node)) + result + .then(resolve, reject) + .finally(() => this.#returnClient(node)) } else { resolve(result); this.#returnClient(node); From d941ec5a4cc73ff8d0965becfdca41a4dc521004 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:58:28 +0300 Subject: [PATCH 200/244] Add Redis 8.2 New Stream Commands (#3029) * chore: update Redis version from 8.2-RC1-pre to 8.2-rc1 * feat: implement XDELEX command for Redis 8.2 * feat: implement XACKDEL command for Redis 8.2 * refactor: create shared stream deletion types for Redis 8.2 commands * feat: add Redis 8.2 deletion policies to XTRIM command * feat: add Redis 8.2 deletion policies to XADD commands * fix: correct XDELEX command method name and test parameter --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/commands/XACKDEL.spec.ts | 196 ++++++++++++++++++ packages/client/lib/commands/XACKDEL.ts | 45 ++++ packages/client/lib/commands/XADD.spec.ts | 80 +++++++ packages/client/lib/commands/XADD.ts | 8 + .../lib/commands/XADD_NOMKSTREAM.spec.ts | 88 +++++++- packages/client/lib/commands/XDELEX.spec.ts | 156 ++++++++++++++ packages/client/lib/commands/XDELEX.ts | 42 ++++ packages/client/lib/commands/XTRIM.spec.ts | 95 ++++++++- packages/client/lib/commands/XTRIM.ts | 8 + .../lib/commands/common-stream.types.ts | 28 +++ packages/client/lib/commands/index.ts | 6 + packages/client/lib/sentinel/test-util.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/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 19 files changed, 746 insertions(+), 22 deletions(-) create mode 100644 packages/client/lib/commands/XACKDEL.spec.ts create mode 100644 packages/client/lib/commands/XACKDEL.ts create mode 100644 packages/client/lib/commands/XDELEX.spec.ts create mode 100644 packages/client/lib/commands/XDELEX.ts create mode 100644 packages/client/lib/commands/common-stream.types.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89efdb61114..df8cb1d1b6c 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.2", "8.2-M01-pre"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2-rc1"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 4396c94f726..268ebca8cb9 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.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/client/lib/commands/XACKDEL.spec.ts b/packages/client/lib/commands/XACKDEL.spec.ts new file mode 100644 index 00000000000..9d7bad15a24 --- /dev/null +++ b/packages/client/lib/commands/XACKDEL.spec.ts @@ -0,0 +1,196 @@ +import { strict as assert } from "node:assert"; +import XACKDEL from "./XACKDEL"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; +import { + STREAM_DELETION_POLICY, + STREAM_DELETION_REPLY_CODES, +} from "./common-stream.types"; + +describe("XACKDEL", () => { + describe("transformArguments", () => { + it("string - without policy", () => { + assert.deepEqual(parseArgs(XACKDEL, "key", "group", "0-0"), [ + "XACKDEL", + "key", + "group", + "IDS", + "1", + "0-0", + ]); + }); + + it("string - with policy", () => { + assert.deepEqual( + parseArgs( + XACKDEL, + "key", + "group", + "0-0", + STREAM_DELETION_POLICY.KEEPREF + ), + ["XACKDEL", "key", "group", "KEEPREF", "IDS", "1", "0-0"] + ); + }); + + it("array - without policy", () => { + assert.deepEqual(parseArgs(XACKDEL, "key", "group", ["0-0", "1-0"]), [ + "XACKDEL", + "key", + "group", + "IDS", + "2", + "0-0", + "1-0", + ]); + }); + + it("array - with policy", () => { + assert.deepEqual( + parseArgs( + XACKDEL, + "key", + "group", + ["0-0", "1-0"], + STREAM_DELETION_POLICY.DELREF + ), + ["XACKDEL", "key", "group", "DELREF", "IDS", "2", "0-0", "1-0"] + ); + }); + }); + + testUtils.testAll( + `XACKDEL non-existing key - without policy`, + async (client) => { + const reply = await client.xAckDel("{tag}stream-key", "testgroup", "0-0"); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL existing key - without policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer group, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel(streamKey, groupName, messageId); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL existing key - with policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer group, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + messageId, + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL acknowledge policy - with consumer group`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer groups, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + await client.xGroupCreate(streamKey, "some-other-group", "0"); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + messageId, + STREAM_DELETION_POLICY.ACKED + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL multiple keys`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer groups, stream and add messages + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageIds = await Promise.all([ + client.xAdd(streamKey, "*", { field: "value1" }), + client.xAdd(streamKey, "*", { field: "value2" }), + ]); + + // read messages + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + [...messageIds, "0-0"], + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [ + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.NOT_FOUND, + ]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); +}); diff --git a/packages/client/lib/commands/XACKDEL.ts b/packages/client/lib/commands/XACKDEL.ts new file mode 100644 index 00000000000..6e209879e49 --- /dev/null +++ b/packages/client/lib/commands/XACKDEL.ts @@ -0,0 +1,45 @@ +import { CommandParser } from "../client/parser"; +import { RedisArgument, ArrayReply, Command } from "../RESP/types"; +import { + StreamDeletionReplyCode, + StreamDeletionPolicy, +} from "./common-stream.types"; +import { RedisVariadicArgument } from "./generic-transformers"; + +/** + * Acknowledges and deletes one or multiple messages for a stream consumer group + */ +export default { + IS_READ_ONLY: false, + /** + * Constructs the XACKDEL command to acknowledge and delete one or multiple messages for a stream consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - The consumer group name + * @param id - One or more message IDs to acknowledge and delete + * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) + * @returns Array of integers: -1 (not found), 1 (acknowledged and deleted), 2 (acknowledged with dangling refs) + * @see https://redis.io/commands/xackdel/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + id: RedisVariadicArgument, + policy?: StreamDeletionPolicy + ) { + parser.push("XACKDEL"); + parser.pushKey(key); + parser.push(group); + + if (policy) { + parser.push(policy); + } + + parser.push("IDS"); + parser.pushVariadicWithLength(id); + }, + transformReply: + undefined as unknown as () => ArrayReply, +} as const satisfies Command; diff --git a/packages/client/lib/commands/XADD.spec.ts b/packages/client/lib/commands/XADD.spec.ts index 321581d0865..a41e8682751 100644 --- a/packages/client/lib/commands/XADD.spec.ts +++ b/packages/client/lib/commands/XADD.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XADD from './XADD'; import { parseArgs } from './generic-transformers'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XADD', () => { describe('transformArguments', () => { @@ -78,6 +79,37 @@ describe('XADD', () => { ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value'] ); }); + + it('with TRIM.policy', () => { + assert.deepEqual( + parseArgs(XADD, 'key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + policy: STREAM_DELETION_POLICY.DELREF + } + }), + ['XADD', 'key', '1000', 'DELREF', '*', 'field', 'value'] + ); + }); + + it('with all TRIM options', () => { + assert.deepEqual( + parseArgs(XADD, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 100, + policy: STREAM_DELETION_POLICY.ACKED + } + }), + ['XADD', 'key', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value'] + ); + }); }); testUtils.testAll('xAdd', async client => { @@ -91,4 +123,52 @@ describe('XADD', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testAll( + 'xAdd with TRIM policy', + async (client) => { + assert.equal( + typeof await client.xAdd('{tag}key', '*', + { field: 'value' }, + { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000, + policy: STREAM_DELETION_POLICY.KEEPREF + } + } + ), + 'string' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + 'xAdd with all TRIM options', + async (client) => { + assert.equal( + typeof await client.xAdd('{tag}key2', '*', + { field: 'value' }, + { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 10, + policy: STREAM_DELETION_POLICY.DELREF + } + } + ), + 'string' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index b0c50b1bfdb..f2509a9fa7b 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -1,5 +1,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { StreamDeletionPolicy } from './common-stream.types'; import { Tail } from './generic-transformers'; /** @@ -10,6 +11,7 @@ import { Tail } from './generic-transformers'; * @property TRIM.strategyModifier - Exact ('=') or approximate ('~') trimming * @property TRIM.threshold - Maximum stream length or minimum ID to retain * @property TRIM.limit - Maximum number of entries to trim in one call + * @property TRIM.policy - Policy to apply when trimming entries (optional, defaults to KEEPREF) */ export interface XAddOptions { TRIM?: { @@ -17,6 +19,8 @@ export interface XAddOptions { strategyModifier?: '=' | '~'; threshold: number; limit?: number; + /** added in 8.2 */ + policy?: StreamDeletionPolicy; }; } @@ -58,6 +62,10 @@ export function parseXAddArguments( if (options.TRIM.limit) { parser.push('LIMIT', options.TRIM.limit.toString()); } + + if (options.TRIM.policy) { + parser.push(options.TRIM.policy); + } } parser.push(id); diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts index 97927f212ff..a957d0f06c1 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts @@ -2,6 +2,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'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XADD NOMKSTREAM', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -80,17 +81,82 @@ describe('XADD NOMKSTREAM', () => { ['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 + it('with TRIM.policy', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + policy: STREAM_DELETION_POLICY.DELREF + } + }), + ['XADD', 'key', 'NOMKSTREAM', '1000', 'DELREF', '*', 'field', 'value'] + ); + }); + + it('with all TRIM options', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 100, + policy: STREAM_DELETION_POLICY.ACKED + } + }), + ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value'] + ); + }); }); + + testUtils.testAll( + 'xAddNoMkStream - null when stream does not exist', + async (client) => { + assert.equal( + await client.xAddNoMkStream('{tag}nonexistent-stream', '*', { + field: 'value' + }), + null + ); + }, + { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + } + ); + + testUtils.testAll( + 'xAddNoMkStream - with all TRIM options', + async (client) => { + const streamKey = '{tag}stream'; + + // Create stream and add some messages + await client.xAdd(streamKey, '*', { field: 'value1' }); + + // Use NOMKSTREAM with all TRIM options + const messageId = await client.xAddNoMkStream(streamKey, '*', + { field: 'value2' }, + { + TRIM: { + strategyModifier: '~', + limit: 1, + strategy: 'MAXLEN', + threshold: 2, + policy: STREAM_DELETION_POLICY.DELREF + } + } + ); + + assert.equal(typeof messageId, 'string'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XDELEX.spec.ts b/packages/client/lib/commands/XDELEX.spec.ts new file mode 100644 index 00000000000..8c421503256 --- /dev/null +++ b/packages/client/lib/commands/XDELEX.spec.ts @@ -0,0 +1,156 @@ +import { strict as assert } from "node:assert"; +import XDELEX from "./XDELEX"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; +import { + STREAM_DELETION_POLICY, + STREAM_DELETION_REPLY_CODES, +} from "./common-stream.types"; + +describe("XDELEX", () => { + describe("transformArguments", () => { + it("string - without policy", () => { + assert.deepEqual(parseArgs(XDELEX, "key", "0-0"), [ + "XDELEX", + "key", + "IDS", + "1", + "0-0", + ]); + }); + + it("string - with policy", () => { + assert.deepEqual( + parseArgs(XDELEX, "key", "0-0", STREAM_DELETION_POLICY.KEEPREF), + ["XDELEX", "key", "KEEPREF", "IDS", "1", "0-0"] + ); + }); + + it("array - without policy", () => { + assert.deepEqual(parseArgs(XDELEX, "key", ["0-0", "1-0"]), [ + "XDELEX", + "key", + "IDS", + "2", + "0-0", + "1-0", + ]); + }); + + it("array - with policy", () => { + assert.deepEqual( + parseArgs(XDELEX, "key", ["0-0", "1-0"], STREAM_DELETION_POLICY.DELREF), + ["XDELEX", "key", "DELREF", "IDS", "2", "0-0", "1-0"] + ); + }); + }); + + testUtils.testAll( + `XDELEX non-existing key - without policy`, + async (client) => { + const reply = await client.xDelEx("{tag}stream-key", "0-0"); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX existing key - without policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + const reply = await client.xDelEx( + streamKey, + messageId, + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX existing key - with policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + const reply = await client.xDelEx( + streamKey, + messageId, + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX acknowledge policy - with consumer group`, + async (client) => { + const streamKey = "{tag}stream-key"; + + // Add a message to the stream + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + // Create consumer group + await client.xGroupCreate(streamKey, "testgroup", "0"); + + const reply = await client.xDelEx( + streamKey, + messageId, + STREAM_DELETION_POLICY.ACKED + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX multiple keys`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageIds = await Promise.all([ + client.xAdd(streamKey, "*", { + field: "value1", + }), + client.xAdd(streamKey, "*", { + field: "value2", + }), + ]); + + const reply = await client.xDelEx( + streamKey, + [...messageIds, "0-0"], + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [ + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.NOT_FOUND, + ]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); +}); diff --git a/packages/client/lib/commands/XDELEX.ts b/packages/client/lib/commands/XDELEX.ts new file mode 100644 index 00000000000..021dd0a9e13 --- /dev/null +++ b/packages/client/lib/commands/XDELEX.ts @@ -0,0 +1,42 @@ +import { CommandParser } from "../client/parser"; +import { RedisArgument, ArrayReply, Command } from "../RESP/types"; +import { + StreamDeletionPolicy, + StreamDeletionReplyCode, +} from "./common-stream.types"; +import { RedisVariadicArgument } from "./generic-transformers"; + +/** + * Deletes one or multiple entries from the stream + */ +export default { + IS_READ_ONLY: false, + /** + * Constructs the XDELEX command to delete one or multiple entries from the stream + * + * @param parser - The command parser + * @param key - The stream key + * @param id - One or more message IDs to delete + * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) + * @returns Array of integers: -1 (not found), 1 (deleted), 2 (dangling refs) + * @see https://redis.io/commands/xdelex/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + id: RedisVariadicArgument, + policy?: StreamDeletionPolicy + ) { + parser.push("XDELEX"); + parser.pushKey(key); + + if (policy) { + parser.push(policy); + } + + parser.push("IDS"); + parser.pushVariadicWithLength(id); + }, + transformReply: + undefined as unknown as () => ArrayReply, +} as const satisfies Command; diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index 2c31f0fef92..b88cf84676e 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XTRIM from './XTRIM'; import { parseArgs } from './generic-transformers'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XTRIM', () => { describe('transformArguments', () => { @@ -12,6 +13,13 @@ describe('XTRIM', () => { ); }); + it('simple - MINID', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MINID', 123), + ['XTRIM', 'key', 'MINID', '123'] + ); + }); + it('with strategyModifier', () => { assert.deepEqual( parseArgs(XTRIM, 'key', 'MAXLEN', 1, { @@ -39,15 +47,96 @@ describe('XTRIM', () => { ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1'] ); }); + + it('with policy', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { + policy: STREAM_DELETION_POLICY.DELREF + }), + ['XTRIM', 'key', 'MAXLEN', '1', 'DELREF'] + ); + }); + + it('with all options', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { + strategyModifier: '~', + LIMIT: 100, + policy: STREAM_DELETION_POLICY.ACKED + }), + ['XTRIM', 'key', 'MAXLEN', '~', '1', 'LIMIT', '100', 'ACKED'] + ); + }); + }); + + testUtils.testAll('xTrim with MAXLEN', async client => { + assert.equal( + typeof await client.xTrim('key', 'MAXLEN', 1), + 'number' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, }); - testUtils.testAll('xTrim', async client => { + testUtils.testAll('xTrim with MINID', async client => { assert.equal( - await client.xTrim('key', 'MAXLEN', 1), - 0 + typeof await client.xTrim('key', 'MINID', 1), + 'number' ); }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN, }); + + testUtils.testAll( + 'xTrim with LIMIT', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MAXLEN', 1000, { + strategyModifier: '~', + LIMIT: 10 + }), + 'number' + ); + }, + { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + } + ); + + testUtils.testAll( + 'xTrim with policy', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MAXLEN', 0, { + policy: STREAM_DELETION_POLICY.DELREF + }), + 'number' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + 'xTrim with all options', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MINID', 0, { + strategyModifier: '~', + LIMIT: 10, + policy: STREAM_DELETION_POLICY.KEEPREF + }), + 'number' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 6125720111a..34171d4611e 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,16 +1,20 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { StreamDeletionPolicy } from './common-stream.types'; /** * Options for the XTRIM command * * @property strategyModifier - Exact ('=') or approximate ('~') trimming * @property LIMIT - Maximum number of entries to trim in one call (Redis 6.2+) + * @property policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) */ export interface XTrimOptions { strategyModifier?: '=' | '~'; /** added in 6.2 */ LIMIT?: number; + /** added in 8.2 */ + policy?: StreamDeletionPolicy; } /** @@ -49,6 +53,10 @@ export default { if (options?.LIMIT) { parser.push('LIMIT', options.LIMIT.toString()); } + + if (options?.policy) { + parser.push(options.policy); + } }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/common-stream.types.ts b/packages/client/lib/commands/common-stream.types.ts new file mode 100644 index 00000000000..60955b6e3c3 --- /dev/null +++ b/packages/client/lib/commands/common-stream.types.ts @@ -0,0 +1,28 @@ +/** Common stream deletion policies + * + * Added in Redis 8.2 + */ +export const STREAM_DELETION_POLICY = { + /** Preserve references (default) */ + KEEPREF: "KEEPREF", + /** Delete all references */ + DELREF: "DELREF", + /** Only acknowledged entries */ + ACKED: "ACKED", +} as const; + +export type StreamDeletionPolicy = + (typeof STREAM_DELETION_POLICY)[keyof typeof STREAM_DELETION_POLICY]; + +/** Common reply codes for stream deletion operations */ +export const STREAM_DELETION_REPLY_CODES = { + /** ID not found */ + NOT_FOUND: -1, + /** Entry deleted */ + DELETED: 1, + /** Dangling references */ + DANGLING_REFS: 2, +} as const; + +export type StreamDeletionReplyCode = + (typeof STREAM_DELETION_REPLY_CODES)[keyof typeof STREAM_DELETION_REPLY_CODES]; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 87ab8d10b8f..4614c8b282b 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -280,6 +280,7 @@ import TYPE from './TYPE'; import UNLINK from './UNLINK'; import WAIT from './WAIT'; import XACK from './XACK'; +import XACKDEL from './XACKDEL'; import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; import XADD from './XADD'; import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; @@ -287,6 +288,7 @@ import XAUTOCLAIM from './XAUTOCLAIM'; import XCLAIM_JUSTID from './XCLAIM_JUSTID'; import XCLAIM from './XCLAIM'; import XDEL from './XDEL'; +import XDELEX from './XDELEX'; import XGROUP_CREATE from './XGROUP_CREATE'; import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; @@ -924,6 +926,8 @@ export default { wait: WAIT, XACK, xAck: XACK, + XACKDEL, + xAckDel: XACKDEL, XADD_NOMKSTREAM, xAddNoMkStream: XADD_NOMKSTREAM, XADD, @@ -938,6 +942,8 @@ export default { xClaim: XCLAIM, XDEL, xDel: XDEL, + XDELEX, + xDelEx: XDELEX, XGROUP_CREATE, xGroupCreate: XGROUP_CREATE, XGROUP_CREATECONSUMER, diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index c8efa47f41d..7c4752a8852 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index b9b906e943c..86b6ed294ac 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 3c561d4ba44..e5d977a6b40 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.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 6b6859d61bf..cba2d95e737 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.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index a2b9c816da1..d4d91307b99 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.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 8a664ee8df2..9c59918e705 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.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { From c2dc73c5d8eb1009d23645eb9f5fed4c59465711 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:00:46 +0100 Subject: [PATCH 201/244] docs: DOC-5473 added time series doc examples (#3030) --- doctests/dt-time-series.js | 635 +++++++++++++++++++++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 doctests/dt-time-series.js diff --git a/doctests/dt-time-series.js b/doctests/dt-time-series.js new file mode 100644 index 00000000000..d2d94e7dc40 --- /dev/null +++ b/doctests/dt-time-series.js @@ -0,0 +1,635 @@ +// EXAMPLE: time_series_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; +import { TIME_SERIES_AGGREGATION_TYPE, TIME_SERIES_REDUCERS } from '@redis/time-series'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del([ + 'thermometer:1', 'thermometer:2', 'thermometer:3', + 'rg:1', 'rg:2', 'rg:3', 'rg:4', + 'sensor3', + 'wind:1', 'wind:2', 'wind:3', 'wind:4', + 'hyg:1', 'hyg:compacted' +]); +// REMOVE_END + +// STEP_START create +const res1 = await client.ts.create('thermometer:1'); +console.log(res1); // >>> OK + +const res2 = await client.type('thermometer:1'); +console.log(res2); // >>> TSDB-TYPE + +const res3 = await client.ts.info('thermometer:1'); +console.log(res3); +// >>> { rules: [], ... totalSamples: 0, ... +// STEP_END +// REMOVE_START +assert.equal(res1, 'OK'); +assert.equal(res2, 'TSDB-TYPE'); +assert.equal(res3.totalSamples, 0); +// REMOVE_END + +// STEP_START create_retention +const res4 = await client.ts.add('thermometer:2', 1, 10.8, { RETENTION: 100 }); +console.log(res4); // >>> 1 + +const res5 = await client.ts.info('thermometer:2'); +console.log(res5); +// >>> { rules: [], ... retentionTime: 100, ... +// STEP_END +// REMOVE_START +assert.equal(res4, 1); +assert.equal(res5.retentionTime, 100); +// REMOVE_END + +// STEP_START create_labels +const res6 = await client.ts.add('thermometer:3', 1, 10.4, { + LABELS: { location: 'UK', type: 'Mercury' } +}); +console.log(res6); // >>> 1 + +const res7 = await client.ts.info('thermometer:3'); +console.log(res7); +// >>> { labels: [{ name: 'location', value: 'UK' }, { name: 'type', value: 'Mercury' }], ... } +// STEP_END +// REMOVE_START +assert.equal(res6, 1); +assert.deepEqual(res7.labels, [ + { name: 'location', value: 'UK' }, + { name: 'type', value: 'Mercury' }, +]); +// REMOVE_END + +// STEP_START madd +const res8 = await client.ts.mAdd([ + { key: 'thermometer:1', timestamp: 1, value: 9.2 }, + { key: 'thermometer:1', timestamp: 2, value: 9.9 }, + { key: 'thermometer:2', timestamp: 2, value: 10.3 } +]); +console.log(res8); // >>> [1, 2, 2] +// STEP_END +// REMOVE_START +assert.deepEqual(res8, [1, 2, 2]); +// REMOVE_END + +// STEP_START get +// The last recorded temperature for thermometer:2 +// was 10.3 at time 2. +const res9 = await client.ts.get('thermometer:2'); +console.log(res9); // >>> { timestamp: 2, value: 10.3 } +// STEP_END +// REMOVE_START +assert.equal(res9.timestamp, 2); +assert.equal(res9.value, 10.3); +// REMOVE_END + +// STEP_START range +// Add 5 data points to a time series named "rg:1". +const res10 = await client.ts.create('rg:1'); +console.log(res10); // >>> OK + +const res11 = await client.ts.mAdd([ + { key: 'rg:1', timestamp: 0, value: 18 }, + { key: 'rg:1', timestamp: 1, value: 14 }, + { key: 'rg:1', timestamp: 2, value: 22 }, + { key: 'rg:1', timestamp: 3, value: 18 }, + { key: 'rg:1', timestamp: 4, value: 24 } +]); +console.log(res11); // >>> [0, 1, 2, 3, 4] + +// Retrieve all the data points in ascending order. +const res12 = await client.ts.range('rg:1', '-', '+'); +console.log(res12); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }, ...] + +// Retrieve data points up to time 1 (inclusive). +const res13 = await client.ts.range('rg:1', '-', 1); +console.log(res13); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }] + +// Retrieve data points from time 3 onwards. +const res14 = await client.ts.range('rg:1', 3, '+'); +console.log(res14); +// >>> [{ timestamp: 3, value: 18 }, { timestamp: 4, value: 24 }] + +// Retrieve all the data points in descending order. +const res15 = await client.ts.revRange('rg:1', '-', '+'); +console.log(res15); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 3, value: 18 }, ...] + +// Retrieve data points up to time 1 (inclusive), but return them +// in descending order. +const res16 = await client.ts.revRange('rg:1', '-', 1); +console.log(res16); +// >>> [{ timestamp: 1, value: 14 }, { timestamp: 0, value: 18 }] +// STEP_END +// REMOVE_START +assert.equal(res10, 'OK'); +assert.deepEqual(res11, [0, 1, 2, 3, 4]); + +assert.deepEqual(res12, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 }, + { timestamp: 2, value: 22 }, + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res13, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 } +]); +assert.deepEqual(res14, [ + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res15, [ + { timestamp: 4, value: 24 }, + { timestamp: 3, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +assert.deepEqual(res16, [ + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +// REMOVE_END + +// STEP_START range_filter +const res17 = await client.ts.range('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4] +}); +console.log(res17); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 2, value: 22 }, { timestamp: 4, value: 24 }] + +const res18 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 20, max: 25 } +}); +console.log(res18); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 2, value: 22 }] + +const res19 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 22, max: 22 }, + COUNT: 1 +}); +console.log(res19); +// >>> [{ timestamp: 2, value: 22 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res17, [ + { timestamp: 0, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res18, [ + { timestamp: 4, value: 24 }, + { timestamp: 2, value: 22 } +]); +assert.deepEqual(res19, [ + { timestamp: 2, value: 22 } +]); +// REMOVE_END + +// STEP_START query_multi +// Create three new "rg:" time series (two in the US +// and one in the UK, with different units) and add some +// data points. +const res20 = await client.ts.create('rg:2', { + LABELS: { location: 'us', unit: 'cm' } +}); +console.log(res20); // >>> OK + +const res21 = await client.ts.create('rg:3', { + LABELS: { location: 'us', unit: 'in' } +}); +console.log(res21); // >>> OK + +const res22 = await client.ts.create('rg:4', { + LABELS: { location: 'uk', unit: 'mm' } +}); +console.log(res22); // >>> OK + +const res23 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 0, value: 1.8 }, + { key: 'rg:3', timestamp: 0, value: 0.9 }, + { key: 'rg:4', timestamp: 0, value: 25 } +]); +console.log(res23); // >>> [0, 0, 0] + +const res24 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 1, value: 2.1 }, + { key: 'rg:3', timestamp: 1, value: 0.77 }, + { key: 'rg:4', timestamp: 1, value: 18 } +]); +console.log(res24); // >>> [1, 1, 1] + +const res25 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 2, value: 2.3 }, + { key: 'rg:3', timestamp: 2, value: 1.1 }, + { key: 'rg:4', timestamp: 2, value: 21 } +]); +console.log(res25); // >>> [2, 2, 2] + +const res26 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 3, value: 1.9 }, + { key: 'rg:3', timestamp: 3, value: 0.81 }, + { key: 'rg:4', timestamp: 3, value: 19 } +]); +console.log(res26); // >>> [3, 3, 3] + +const res27 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 4, value: 1.78 }, + { key: 'rg:3', timestamp: 4, value: 0.74 }, + { key: 'rg:4', timestamp: 4, value: 23 } +]); +console.log(res27); // >>> [4, 4, 4] + +// Retrieve the last data point from each US time series. +const res28 = await client.ts.mGet(['location=us']); +console.log(res28); +// >>> { "rg:2": { sample: { timestamp: 4, value: 1.78 } }, "rg:3": { sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve the same data points, but include the `unit` +// label in the results. +const res29 = await client.ts.mGetSelectedLabels(['location=us'], ['unit']); +console.log(res29); +// >>> { "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve data points up to time 2 (inclusive) from all +// time series that use millimeters as the unit. Include all +// labels in the results. +const res30 = await client.ts.mRangeWithLabels('-', 2, 'unit=mm'); +console.log(res30); +// >>> { "rg:4": { labels: { location: 'uk', unit: 'mm' }, samples: [ +// { timestamp: 0, value: 25 }, +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 } +// ] } } + +// Retrieve data points from time 1 to time 3 (inclusive) from +// all time series that use centimeters or millimeters as the unit, +// but only return the `location` label. Return the results +// in descending order of timestamp. +const res31 = await client.ts.mRevRangeSelectedLabels( + 1, 3, + ['location'], + ['unit=(cm,mm)'] +); +console.log(res31); +// >>> { "rg:2": { labels: { location: 'us' }, samples: [ +// { timestamp: 3, value: 1.9 }, +// { timestamp: 2, value: 2.3 }, +// { timestamp: 1, value: 2.1 } +// ] }, "rg:4": { labels: { location: 'uk' }, samples: [ +// { timestamp: 3, value: 19 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 1, value: 18 } +// ] } } +// STEP_END +// REMOVE_START +assert.equal(res20, 'OK'); +assert.equal(res21, 'OK'); +assert.equal(res22, 'OK'); +assert.deepEqual(res23, [0, 0, 0]); +assert.deepEqual(res24, [1, 1, 1]); +assert.deepEqual(res25, [2, 2, 2]); +assert.deepEqual(res26, [3, 3, 3]); +assert.deepEqual(res27, [4, 4, 4]); + +assert.deepEqual(res28, { + "rg:2": { sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { sample: { timestamp: 4, value: 0.74 } } +}); +assert.deepEqual(res29, { + "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } +}); + +assert.deepEqual(res30, { + "rg:4": { + labels: { location: 'uk', unit: 'mm' }, + samples: [ + { timestamp: 0, value: 25 }, + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 } + ] + } +}); +assert.deepEqual(res31, { + "rg:2": { + labels: { location: 'us' }, + samples: [ + { timestamp: 3, value: 1.9 }, + { timestamp: 2, value: 2.3 }, + { timestamp: 1, value: 2.1 } + ] + }, + "rg:4": { + labels: { location: 'uk' }, + samples: [ + { timestamp: 3, value: 19 }, + { timestamp: 2, value: 21 }, + { timestamp: 1, value: 18 } + ] + } +}); +// REMOVE_END + +// STEP_START agg +const res32 = await client.ts.range('rg:2', '-', '+', { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 2 + } +}); +console.log(res32); +// >>> [{ timestamp: 0, value: 1.9500000000000002 },{ timestamp: 2, value: 2.0999999999999996 }, { timestamp: 4, value: 1.78 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res32, [ + { timestamp: 0, value: 1.9500000000000002 }, + { timestamp: 2, value: 2.0999999999999996 }, + { timestamp: 4, value: 1.78 } +]); +// REMOVE_END + +// STEP_START agg_bucket +const res33 = await client.ts.create('sensor3'); +console.log(res33); // >>> OK + +const res34 = await client.ts.mAdd([ + { key: 'sensor3', timestamp: 10, value: 1000 }, + { key: 'sensor3', timestamp: 20, value: 2000 }, + { key: 'sensor3', timestamp: 30, value: 3000 }, + { key: 'sensor3', timestamp: 40, value: 4000 }, + { key: 'sensor3', timestamp: 50, value: 5000 }, + { key: 'sensor3', timestamp: 60, value: 6000 }, + { key: 'sensor3', timestamp: 70, value: 7000 } +]); +console.log(res34); // >>> [10, 20, 30, 40, 50, 60, 70] + +const res35 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + } +}); +console.log(res35); +// >>> [{ timestamp: 0, value: 1000 }, { timestamp: 25, value: 3000 }, { timestamp: 50, value: 5000 }] +// STEP_END +// REMOVE_START +assert.equal(res33, 'OK'); +assert.deepEqual(res34, [10, 20, 30, 40, 50, 60, 70]); +assert.deepEqual(res35, [ + { timestamp: 0, value: 1000 }, + { timestamp: 25, value: 3000 }, + { timestamp: 50, value: 5000 } +]); +// REMOVE_END + +// STEP_START agg_align +const res36 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + }, + ALIGN: 'START' +}); +console.log(res36); +// >>> [{ timestamp: 10, value: 1000 }, { timestamp: 35, value: 4000 }, { timestamp: 60, value: 6000 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res36, [ + { timestamp: 10, value: 1000 }, + { timestamp: 35, value: 4000 }, + { timestamp: 60, value: 6000 } +]); +// REMOVE_END + +// STEP_START agg_multi +const res37 = await client.ts.create('wind:1', { + LABELS: { country: 'uk' } +}); +console.log(res37); // >>> OK + +const res38 = await client.ts.create('wind:2', { + LABELS: { country: 'uk' } +}); +console.log(res38); // >>> OK + +const res39 = await client.ts.create('wind:3', { + LABELS: { country: 'us' } +}); +console.log(res39); // >>> OK + +const res40 = await client.ts.create('wind:4', { + LABELS: { country: 'us' } +}); +console.log(res40); // >>> OK + +const res41 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 1, value: 12 }, + { key: 'wind:2', timestamp: 1, value: 18 }, + { key: 'wind:3', timestamp: 1, value: 5 }, + { key: 'wind:4', timestamp: 1, value: 20 } +]); +console.log(res41); // >>> [1, 1, 1, 1] + +const res42 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 2, value: 14 }, + { key: 'wind:2', timestamp: 2, value: 21 }, + { key: 'wind:3', timestamp: 2, value: 4 }, + { key: 'wind:4', timestamp: 2, value: 25 } +]); +console.log(res42); // >>> [2, 2, 2, 2] + +const res43 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 3, value: 10 }, + { key: 'wind:2', timestamp: 3, value: 24 }, + { key: 'wind:3', timestamp: 3, value: 8 }, + { key: 'wind:4', timestamp: 3, value: 18 } +]); +console.log(res43); // >>> [3, 3, 3, 3] + +// The result pairs contain the timestamp and the maximum sample value +// for the country at that timestamp. +const res44 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + {label: 'country', REDUCE: TIME_SERIES_REDUCERS.MAX} +); +console.log(res44); +// >>> { "country=uk": { samples: [ +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 3, value: 24 } +// ] }, "country=us": { samples: [ +// { timestamp: 1, value: 20 }, +// { timestamp: 2, value: 25 }, +// { timestamp: 3, value: 18 } +// ] } } + +// The result pairs contain the timestamp and the average sample value +// for the country at that timestamp. +const res45 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + { label: 'country', REDUCE: TIME_SERIES_REDUCERS.AVG} +); +console.log(res45); +// >>> { +// "country=uk": { +// samples: [{ timestamp: 1, value: 15 }, { timestamp: 2, value: 17.5 }, { timestamp: 3, value: 17 }] +// }, +// "country=us": { +// samples: [{ timestamp: 1, value: 12.5 }, { timestamp: 2, value: 14.5 }, { timestamp: 3, value: 13 }] +// } +// } +// STEP_END +// REMOVE_START +assert.equal(res37, 'OK'); +assert.equal(res38, 'OK'); +assert.equal(res39, 'OK'); +assert.equal(res40, 'OK'); +assert.deepEqual(res41, [1, 1, 1, 1]); +assert.deepEqual(res42, [2, 2, 2, 2]); +assert.deepEqual(res43, [3, 3, 3, 3]); + +assert.deepEqual(res44, { + "country=uk": { + samples: [ + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 }, + { timestamp: 3, value: 24 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 20 }, + { timestamp: 2, value: 25 }, + { timestamp: 3, value: 18 } + ] + } +}); +assert.deepEqual(res45, { + "country=uk": { + samples: [ + { timestamp: 1, value: 15 }, + { timestamp: 2, value: 17.5 }, + { timestamp: 3, value: 17 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 12.5 }, + { timestamp: 2, value: 14.5 }, + { timestamp: 3, value: 13 } + ] + } +}); +// REMOVE_END + +// STEP_START create_compaction +const res46 = await client.ts.create('hyg:1'); +console.log(res46); // >>> OK + +const res47 = await client.ts.create('hyg:compacted'); +console.log(res47); // >>> OK + +const res48 = await client.ts.createRule('hyg:1', 'hyg:compacted', TIME_SERIES_AGGREGATION_TYPE.MIN, 3); +console.log(res48); // >>> OK + +const res49 = await client.ts.info('hyg:1'); +console.log(res49.rules); +// >>> [{ aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3}] + +const res50 = await client.ts.info('hyg:compacted'); +console.log(res50.sourceKey); // >>> 'hyg:1' +// STEP_END +// REMOVE_START +assert.equal(res46, 'OK'); +assert.equal(res47, 'OK'); +assert.equal(res48, 'OK'); +assert.deepEqual(res49.rules, [ + { aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3} +]); +assert.equal(res50.sourceKey, 'hyg:1'); +// REMOVE_END + +// STEP_START comp_add +const res51 = await client.ts.mAdd([ + { key: 'hyg:1', timestamp: 0, value: 75 }, + { key: 'hyg:1', timestamp: 1, value: 77 }, + { key: 'hyg:1', timestamp: 2, value: 78 } +]); +console.log(res51); // >>> [0, 1, 2] + +const res52 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res52); // >>> [] + +const res53 = await client.ts.add('hyg:1', 3, 79); +console.log(res53); // >>> 3 + +const res54 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res54); // >>> [{ timestamp: 0, value: 75 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res51, [0, 1, 2]); +assert.deepEqual(res52, []); +assert.equal(res53, 3); +assert.deepEqual(res54, [{ timestamp: 0, value: 75 }]); +// REMOVE_END + +// STEP_START del +const res55 = await client.ts.info('thermometer:1'); +console.log(res55.totalSamples); // >>> 2 +console.log(res55.firstTimestamp); // >>> 1 +console.log(res55.lastTimestamp); // >>> 2 + +const res56 = await client.ts.add('thermometer:1', 3, 9.7); +console.log(res56); // >>> 3 + +const res57 = await client.ts.info('thermometer:1'); +console.log(res57.totalSamples); // >>> 3 +console.log(res57.firstTimestamp); // >>> 1 +console.log(res57.lastTimestamp); // >>> 3 + +const res58 = await client.ts.del('thermometer:1', 1, 2); +console.log(res58); // >>> 2 + +const res59 = await client.ts.info('thermometer:1'); +console.log(res59.totalSamples); // >>> 1 +console.log(res59.firstTimestamp); // >>> 3 +console.log(res59.lastTimestamp); // >>> 3 + +const res60 = await client.ts.del('thermometer:1', 3, 3); +console.log(res60); // >>> 1 + +const res61 = await client.ts.info('thermometer:1'); +console.log(res61.totalSamples); // >>> 0 +// STEP_END +// REMOVE_START +assert.equal(res55.totalSamples, 2); +assert.equal(res55.firstTimestamp, 1); +assert.equal(res55.lastTimestamp, 2); +assert.equal(res56, 3); +assert.equal(res57.totalSamples, 3); +assert.equal(res57.firstTimestamp, 1); +assert.equal(res57.lastTimestamp, 3); +assert.equal(res58, 2); +assert.equal(res59.totalSamples, 1); +assert.equal(res59.firstTimestamp, 3); +assert.equal(res59.lastTimestamp, 3); +assert.equal(res60, 1); +assert.equal(res61.totalSamples, 0); +// REMOVE_END + +// HIDE_START +await client.quit(); +// HIDE_END \ No newline at end of file From 28d719d699b41023fa63ba616cc6931d927d5132 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:01:05 +0100 Subject: [PATCH 202/244] docs: DOC-5074 added vector set doc examples (#3031) --- doctests/dt-vec-set.js | 281 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 doctests/dt-vec-set.js diff --git a/doctests/dt-vec-set.js b/doctests/dt-vec-set.js new file mode 100644 index 00000000000..0e8cb918d7e --- /dev/null +++ b/doctests/dt-vec-set.js @@ -0,0 +1,281 @@ +// EXAMPLE: vecset_tutorial +// REMOVE_START +/** + * Code samples for Vector set doc pages: + * https://redis.io/docs/latest/develop/data-types/vector-sets/ + */ + +import assert from 'assert'; +// REMOVE_END +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient({ + RESP: 3 // Required for vector set commands +}); + +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del([ + "points", "quantSetQ8", "quantSetNoQ", + "quantSetBin", "setNotReduced", "setReduced" +]); +// REMOVE_END + +// STEP_START vadd +const res1 = await client.vAdd("points", [1.0, 1.0], "pt:A"); +console.log(res1); // >>> true + +const res2 = await client.vAdd("points", [-1.0, -1.0], "pt:B"); +console.log(res2); // >>> true + +const res3 = await client.vAdd("points", [-1.0, 1.0], "pt:C"); +console.log(res3); // >>> true + +const res4 = await client.vAdd("points", [1.0, -1.0], "pt:D"); +console.log(res4); // >>> true + +const res5 = await client.vAdd("points", [1.0, 0], "pt:E"); +console.log(res5); // >>> true + +const res6 = await client.type("points"); +console.log(res6); // >>> vectorset +// STEP_END +// REMOVE_START +assert.equal(res1, true); +assert.equal(res2, true); +assert.equal(res3, true); +assert.equal(res4, true); +assert.equal(res5, true); +assert.equal(res6, "vectorset"); +// REMOVE_END + +// STEP_START vcardvdim +const res7 = await client.vCard("points"); +console.log(res7); // >>> 5 + +const res8 = await client.vDim("points"); +console.log(res8); // >>> 2 +// STEP_END +// REMOVE_START +assert.equal(res7, 5); +assert.equal(res8, 2); +// REMOVE_END + +// STEP_START vemb +const res9 = await client.vEmb("points", "pt:A"); +console.log(res9); // >>> [0.9999999403953552, 0.9999999403953552] + +const res10 = await client.vEmb("points", "pt:B"); +console.log(res10); // >>> [-0.9999999403953552, -0.9999999403953552] + +const res11 = await client.vEmb("points", "pt:C"); +console.log(res11); // >>> [-0.9999999403953552, 0.9999999403953552] + +const res12 = await client.vEmb("points", "pt:D"); +console.log(res12); // >>> [0.9999999403953552, -0.9999999403953552] + +const res13 = await client.vEmb("points", "pt:E"); +console.log(res13); // >>> [1, 0] +// STEP_END +// REMOVE_START +assert(Math.abs(1 - res9[0]) < 0.001); +assert(Math.abs(1 - res9[1]) < 0.001); +assert(Math.abs(1 + res10[0]) < 0.001); +assert(Math.abs(1 + res10[1]) < 0.001); +assert(Math.abs(1 + res11[0]) < 0.001); +assert(Math.abs(1 - res11[1]) < 0.001); +assert(Math.abs(1 - res12[0]) < 0.001); +assert(Math.abs(1 + res12[1]) < 0.001); +assert.deepEqual(res13, [1, 0]); +// REMOVE_END + +// STEP_START attr +const res14 = await client.vSetAttr("points", "pt:A", { + name: "Point A", + description: "First point added" +}); +console.log(res14); // >>> true + +const res15 = await client.vGetAttr("points", "pt:A"); +console.log(res15); +// >>> {name: 'Point A', description: 'First point added'} + +const res16 = await client.vSetAttr("points", "pt:A", ""); +console.log(res16); // >>> true + +const res17 = await client.vGetAttr("points", "pt:A"); +console.log(res17); // >>> null +// STEP_END +// REMOVE_START +assert.equal(res14, true); +assert.deepEqual(res15, {name: "Point A", description: "First point added"}); +assert.equal(res16, true); +assert.equal(res17, null); +// REMOVE_END + +// STEP_START vrem +const res18 = await client.vAdd("points", [0, 0], "pt:F"); +console.log(res18); // >>> true + +const res19 = await client.vCard("points"); +console.log(res19); // >>> 6 + +const res20 = await client.vRem("points", "pt:F"); +console.log(res20); // >>> true + +const res21 = await client.vCard("points"); +console.log(res21); // >>> 5 +// STEP_END +// REMOVE_START +assert.equal(res18, true); +assert.equal(res19, 6); +assert.equal(res20, true); +assert.equal(res21, 5); +// REMOVE_END + +// STEP_START vsim_basic +const res22 = await client.vSim("points", [0.9, 0.1]); +console.log(res22); +// >>> ['pt:E', 'pt:A', 'pt:D', 'pt:C', 'pt:B'] +// STEP_END +// REMOVE_START +assert.deepEqual(res22, ["pt:E", "pt:A", "pt:D", "pt:C", "pt:B"]); +// REMOVE_END + +// STEP_START vsim_options +const res23 = await client.vSimWithScores("points", "pt:A", { COUNT: 4 }); +console.log(res23); +// >>> {pt:A: 1.0, pt:E: 0.8535534143447876, pt:D: 0.5, pt:C: 0.5} +// STEP_END +// REMOVE_START +assert.equal(res23["pt:A"], 1.0); +assert.equal(res23["pt:C"], 0.5); +assert.equal(res23["pt:D"], 0.5); +assert(Math.abs(res23["pt:E"] - 0.85) < 0.005); +// REMOVE_END + +// STEP_START vsim_filter +const res24 = await client.vSetAttr("points", "pt:A", { + size: "large", + price: 18.99 +}); +console.log(res24); // >>> true + +const res25 = await client.vSetAttr("points", "pt:B", { + size: "large", + price: 35.99 +}); +console.log(res25); // >>> true + +const res26 = await client.vSetAttr("points", "pt:C", { + size: "large", + price: 25.99 +}); +console.log(res26); // >>> true + +const res27 = await client.vSetAttr("points", "pt:D", { + size: "small", + price: 21.00 +}); +console.log(res27); // >>> true + +const res28 = await client.vSetAttr("points", "pt:E", { + size: "small", + price: 17.75 +}); +console.log(res28); // >>> true + +// Return elements in order of distance from point A whose +// `size` attribute is `large`. +const res29 = await client.vSim("points", "pt:A", { + FILTER: '.size == "large"' +}); +console.log(res29); // >>> ['pt:A', 'pt:C', 'pt:B'] + +// Return elements in order of distance from point A whose size is +// `large` and whose price is greater than 20.00. +const res30 = await client.vSim("points", "pt:A", { + FILTER: '.size == "large" && .price > 20.00' +}); +console.log(res30); // >>> ['pt:C', 'pt:B'] +// STEP_END +// REMOVE_START +assert.equal(res24, true); +assert.equal(res25, true); +assert.equal(res26, true); +assert.equal(res27, true); +assert.equal(res28, true); +assert.deepEqual(res29, ['pt:A', 'pt:C', 'pt:B']); +assert.deepEqual(res30, ['pt:C', 'pt:B']); +// REMOVE_END + +// STEP_START add_quant +const res31 = await client.vAdd("quantSetQ8", [1.262185, 1.958231], "quantElement", { + QUANT: 'Q8' +}); +console.log(res31); // >>> true + +const res32 = await client.vEmb("quantSetQ8", "quantElement"); +console.log(`Q8: ${res32}`); +// >>> Q8: [1.2643694877624512, 1.958230972290039] + +const res33 = await client.vAdd("quantSetNoQ", [1.262185, 1.958231], "quantElement", { + QUANT: 'NOQUANT' +}); +console.log(res33); // >>> true + +const res34 = await client.vEmb("quantSetNoQ", "quantElement"); +console.log(`NOQUANT: ${res34}`); +// >>> NOQUANT: [1.262184977531433, 1.958230972290039] + +const res35 = await client.vAdd("quantSetBin", [1.262185, 1.958231], "quantElement", { + QUANT: 'BIN' +}); +console.log(res35); // >>> true + +const res36 = await client.vEmb("quantSetBin", "quantElement"); +console.log(`BIN: ${res36}`); +// >>> BIN: [1, 1] +// STEP_END +// REMOVE_START +assert.equal(res31, true); +assert(Math.abs(res32[0] - 1.2643694877624512) < 0.001); +assert(Math.abs(res32[1] - 1.958230972290039) < 0.001); +assert.equal(res33, true); +assert(Math.abs(res34[0] - 1.262184977531433) < 0.001); +assert(Math.abs(res34[1] - 1.958230972290039) < 0.001); +assert.equal(res35, true); +assert.deepEqual(res36, [1, 1]); +// REMOVE_END + +// STEP_START add_reduce +// Create a list of 300 arbitrary values. +const values = Array.from({length: 300}, (_, x) => x / 299); + +const res37 = await client.vAdd("setNotReduced", values, "element"); +console.log(res37); // >>> true + +const res38 = await client.vDim("setNotReduced"); +console.log(res38); // >>> 300 + +const res39 = await client.vAdd("setReduced", values, "element", { + REDUCE: 100 +}); +console.log(res39); // >>> true + +const res40 = await client.vDim("setReduced"); +console.log(res40); // >>> 100 +// STEP_END +// REMOVE_START +assert.equal(res37, true); +assert.equal(res38, 300); +assert.equal(res39, true); +assert.equal(res40, 100); +// REMOVE_END + +// HIDE_START +await client.quit(); +// HIDE_END From d8e14fa4febe69cfee942a477f99dd2492e8ff18 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 28 Jul 2025 14:54:50 +0300 Subject: [PATCH 203/244] fix(scan): remove console.logs (#3038) fixes #3037 --- packages/client/lib/commands/SCAN.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 173e8959afa..d3153b786f1 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -87,7 +87,6 @@ export default { if (options?.TYPE) { parser.push('TYPE', options.TYPE); } - console.log('eeeeeeeeee', parser.redisArgs) }, /** * Transforms the SCAN reply into a structured object @@ -96,7 +95,6 @@ export default { * @returns Object with cursor and keys properties */ transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { - console.log(cursor, keys) return { cursor, keys From 5f09e4a8a58cfaa093a9860724f812c1c19e0d33 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:11:51 +0300 Subject: [PATCH 204/244] feat: add EPSILON parameter support to VSIM command (#3041) --- packages/client/lib/commands/VSIM.spec.ts | 30 ++++++++++++++++++++--- packages/client/lib/commands/VSIM.ts | 5 ++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/commands/VSIM.spec.ts b/packages/client/lib/commands/VSIM.spec.ts index b7e10eb6c49..dbfad76fd59 100644 --- a/packages/client/lib/commands/VSIM.spec.ts +++ b/packages/client/lib/commands/VSIM.spec.ts @@ -31,14 +31,15 @@ describe('VSIM', () => { FILTER: '.price > 20', 'FILTER-EF': 50, TRUTH: true, - NOTHREAD: true + NOTHREAD: true, + EPSILON: 0.1 }); assert.deepEqual( parser.redisArgs, [ - 'VSIM', 'key', 'ELE', 'element', - 'COUNT', '5', 'EF', '100', 'FILTER', '.price > 20', - 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD' + 'VSIM', 'key', 'ELE', 'element', 'COUNT', '5', + 'EPSILON', '0.1', 'EF', '100', 'FILTER', '.price > 20', + 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD', ] ); }); @@ -56,6 +57,27 @@ describe('VSIM', () => { cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } }); + + testUtils.testAll('vSim with options', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSim('key', 'element1', { + EPSILON: 0.1, + COUNT: 1, + EF: 100, + FILTER: '.year == 8', + 'FILTER-EF': 50, + TRUTH: true, + NOTHREAD: true + }); + + assert.ok(Array.isArray(result)); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + testUtils.testWithClient('vSim with RESP3', async client => { await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts index dc41a54caf1..7c94cd7c79d 100644 --- a/packages/client/lib/commands/VSIM.ts +++ b/packages/client/lib/commands/VSIM.ts @@ -4,6 +4,7 @@ import { transformDoubleArgument } from './generic-transformers'; export interface VSimOptions { COUNT?: number; + EPSILON?: number; EF?: number; FILTER?: string; 'FILTER-EF'?: number; @@ -44,6 +45,10 @@ export default { parser.push('COUNT', options.COUNT.toString()); } + if (options?.EPSILON !== undefined) { + parser.push('EPSILON', options.EPSILON.toString()); + } + if (options?.EF !== undefined) { parser.push('EF', options.EF.toString()); } From facc91847e5605fbe8ecaa8dd44a8b913c495e57 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:03 +0000 Subject: [PATCH 205/244] Release client@5.7.0 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71e0ce79df8..640861530b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", + "integrity": "sha512-bWHmSFIJ5w1Y4aHsYs46XMDHKQsBHFRhNcllYaBxz2Zl+lu+gbm5yI9BqxvKh48bLTs/Wx1Kns0gN2WIasE8MA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.1", diff --git a/packages/client/package.json b/packages/client/package.json index 9fe67df521e..091ddaf5c91 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 69fa90784344b3e5e7b26b2925effbe288f974a4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:11 +0000 Subject: [PATCH 206/244] Release bloom@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 640861530b2..79a9493d3c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.1.tgz", + "integrity": "sha512-5/22U76IMEfn6TeZ+uvjXspHw+ykBF0kpBa8xouzeHaQMXs/auqBUOEYzU2VKYDvnd2RSpPTyIg82oB7PpUgLg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index ef900b00a6d..edc106214e0 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From b5cf002751853bb8fe17207efbd15a66b4958e93 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:18 +0000 Subject: [PATCH 207/244] Release json@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79a9493d3c8..2fda06e6a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.1.tgz", + "integrity": "sha512-cTggVzPIVuiFeXcEcnTRiUzV7rmUvM9KUYxWiHyjsAVACTEUe4ifKkvzrij0H/z3ammU5tfGACffDB3olBwtVA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.1", diff --git a/packages/json/package.json b/packages/json/package.json index e62377feb14..fac6209c360 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From f37f98aced7dd76b55c2b39b053b0e05b1f98806 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:26 +0000 Subject: [PATCH 208/244] Release search@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fda06e6a8c..52d8b4f2a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.6.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.1.tgz", + "integrity": "sha512-+eOjx8O2YoKygjqkLpTHqcAq0zKLjior+ee2tRBx/3RSf1+OHxiC9Y6NstshQpvB1XHqTw9n7+f0+MsRJZrp0g==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index c8fb49d5332..5d7fffcfd26 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From fa3ca983a3e1fda3a81f254ba9dd82cd6af12df4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:34 +0000 Subject: [PATCH 209/244] Release time-series@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52d8b4f2a6e..c9e584024b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.6.1" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.1.tgz", + "integrity": "sha512-sd3q4jMJdoSO2akw1L9NrdFI1JJ6zeMgMUoTh4a34p9sY3AnOI4aDLCecy8L2IcPAP1oNR3TbLFJiCJDQ35QTA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.7.0", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 1c42757523f..44fca5643f3 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From c75809ec650883a33ddcdca365c91aa5fbbf4961 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:41 +0000 Subject: [PATCH 210/244] Release entraid@5.7.0 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9e584024b2..eacb323d1a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 4e702fabb65..cb57d9074c3 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@types/express": "^4.17.21", From e2d4b43e39d145e605ca582dbb520b94ab505117 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:49 +0000 Subject: [PATCH 211/244] Release redis@5.7.0 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index eacb323d1a5..5ecc96aff06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.6.1", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.6.1", - "@redis/client": "5.6.1", - "@redis/json": "5.6.1", - "@redis/search": "5.6.1", - "@redis/time-series": "5.6.1" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.1.tgz", - "integrity": "sha512-5/22U76IMEfn6TeZ+uvjXspHw+ykBF0kpBa8xouzeHaQMXs/auqBUOEYzU2VKYDvnd2RSpPTyIg82oB7PpUgLg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", - "integrity": "sha512-bWHmSFIJ5w1Y4aHsYs46XMDHKQsBHFRhNcllYaBxz2Zl+lu+gbm5yI9BqxvKh48bLTs/Wx1Kns0gN2WIasE8MA==", + "version": "5.7.0", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.7.0", + "@redis/client": "5.7.0", + "@redis/json": "5.7.0", + "@redis/search": "5.7.0", + "@redis/time-series": "5.7.0" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.1.tgz", - "integrity": "sha512-cTggVzPIVuiFeXcEcnTRiUzV7rmUvM9KUYxWiHyjsAVACTEUe4ifKkvzrij0H/z3ammU5tfGACffDB3olBwtVA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.1.tgz", - "integrity": "sha512-+eOjx8O2YoKygjqkLpTHqcAq0zKLjior+ee2tRBx/3RSf1+OHxiC9Y6NstshQpvB1XHqTw9n7+f0+MsRJZrp0g==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.1.tgz", - "integrity": "sha512-sd3q4jMJdoSO2akw1L9NrdFI1JJ6zeMgMUoTh4a34p9sY3AnOI4aDLCecy8L2IcPAP1oNR3TbLFJiCJDQ35QTA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.7.0", diff --git a/packages/redis/package.json b/packages/redis/package.json index bdb86663480..a14f6ba10bc 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.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.6.1", - "@redis/client": "5.6.1", - "@redis/json": "5.6.1", - "@redis/search": "5.6.1", - "@redis/time-series": "5.6.1" + "@redis/bloom": "5.7.0", + "@redis/client": "5.7.0", + "@redis/json": "5.7.0", + "@redis/search": "5.7.0", + "@redis/time-series": "5.7.0" }, "engines": { "node": ">= 18" From 66638fc90343aae3f8280829be1673693ec7fc85 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:07:18 +0300 Subject: [PATCH 212/244] Add support svs vamana index creation (#3025) * feat(search): add SVS-VAMANA vector index algorithm support - Add VAMANA algorithm with compression and tuning parameters - Include comprehensive test coverage for various configurations - Fix parameter validation to handle falsy values correctly * feat(search): add additional VAMANA compression algorithms - Add LVQ4, LVQ4x4, LVQ4x8, LeanVec4x8, and LeanVec8x8 compression options - Update test to use LeanVec4x8 compression algorithm * chore: update Redis version from 8.2-rc1 to 8.2-rc2-pre --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/sentinel/test-util.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/CREATE.spec.ts | 112 +++++++++++++++++++- packages/search/lib/commands/CREATE.ts | 81 ++++++++++++-- packages/search/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 10 files changed, 193 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df8cb1d1b6c..89925b55d44 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.4.0-v1", "8.0.2", "8.2-rc1"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2-rc2-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 268ebca8cb9..0f77acae6f6 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.2-rc1' + defaultDockerVersion: '8.2-rc2-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 7c4752a8852..a88c9818580 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-rc1' + defaultDockerVersion: '8.2-rc2-pre' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 86b6ed294ac..62509dee143 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-rc1' + defaultDockerVersion: '8.2-rc2-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index e5d977a6b40..1b63a955bfa 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.2-rc1' + defaultDockerVersion: '8.2-rc2-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 cba2d95e737..81a546fcd64 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.2-rc1' + defaultDockerVersion: '8.2-rc2-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts index 2c54d3d0235..268421ef35f 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -1,6 +1,6 @@ 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 CREATE, { SCHEMA_FIELD_TYPE, SCHEMA_TEXT_FIELD_PHONETIC, SCHEMA_VECTOR_FIELD_ALGORITHM, REDISEARCH_LANGUAGE, VAMANA_COMPRESSION_ALGORITHM } from './CREATE'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CREATE', () => { @@ -206,6 +206,33 @@ describe('FT.CREATE', () => { ] ); }); + + it('VAMANA algorithm', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT32", + COMPRESSION: VAMANA_COMPRESSION_ALGORITHM.LVQ8, + DIM: 1024, + DISTANCE_METRIC: 'COSINE', + CONSTRUCTION_WINDOW_SIZE: 300, + GRAPH_MAX_DEGREE: 128, + SEARCH_WINDOW_SIZE: 20, + EPSILON: 0.02, + TRAINING_THRESHOLD: 20480, + REDUCE: 512, + } + }), + [ + 'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'SVS-VAMANA', '20', 'TYPE', + 'FLOAT32', 'DIM', '1024', 'DISTANCE_METRIC', 'COSINE', 'COMPRESSION', 'LVQ8', + 'CONSTRUCTION_WINDOW_SIZE', '300', 'GRAPH_MAX_DEGREE', '128', 'SEARCH_WINDOW_SIZE', '20', + 'EPSILON', '0.02', 'TRAINING_THRESHOLD', '20480', 'REDUCE', '512' + ] + ); + }); }); describe('GEOSHAPE', () => { @@ -556,4 +583,87 @@ describe('FT.CREATE', () => { "OK" ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 2], 'LATEST'], 'client.ft.create vector svs-vamana', async client => { + assert.equal( + await client.ft.create("index_svs_vamana_min_config", { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT32", + DIM: 768, + DISTANCE_METRIC: 'L2', + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_svs_vamana_no_compression", { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT32", + DIM: 512, + DISTANCE_METRIC: 'L2', + CONSTRUCTION_WINDOW_SIZE: 200, + GRAPH_MAX_DEGREE: 64, + SEARCH_WINDOW_SIZE: 50, + EPSILON: 0.01 + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_svs_vamana_compression", { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT32", + COMPRESSION: VAMANA_COMPRESSION_ALGORITHM.LeanVec4x8, + DIM: 1024, + DISTANCE_METRIC: 'COSINE', + CONSTRUCTION_WINDOW_SIZE: 300, + GRAPH_MAX_DEGREE: 128, + SEARCH_WINDOW_SIZE: 20, + EPSILON: 0.02, + TRAINING_THRESHOLD: 20480, + REDUCE: 512, + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_svs_vamana_float16", { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT16", + DIM: 128, + DISTANCE_METRIC: 'IP', + }, + }), + "OK" + ); + + await assert.rejects( + client.ft.create("index_svs_vamana_invalid_config", { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.VAMANA, + TYPE: "FLOAT32", + DIM: 2, + DISTANCE_METRIC: 'L2', + CONSTRUCTION_WINDOW_SIZE: 200, + GRAPH_MAX_DEGREE: 64, + SEARCH_WINDOW_SIZE: 50, + EPSILON: 0.01, + // TRAINING_THRESHOLD should error without COMPRESSION + TRAINING_THRESHOLD: 2048 + }, + }), + ) + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index 9f24a256fae..3f892df40d2 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -54,7 +54,11 @@ interface SchemaTagField extends SchemaCommonField Date: Tue, 5 Aug 2025 03:12:10 -0400 Subject: [PATCH 213/244] fix: createClient url+tls invariant violation check (#2835) --- packages/client/lib/client/index.spec.ts | 49 +++++++++++++++++++++++- packages/client/lib/client/index.ts | 49 ++++++++++++++++-------- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index f04d6467062..0aed98450d2 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -64,7 +64,8 @@ describe('Client', () => { const expected: RedisClientOptions = { socket: { host: 'localhost', - port: 6379 + port: 6379, + tls: false }, username: 'user', password: 'secret', @@ -161,12 +162,58 @@ describe('Client', () => { { socket: { host: 'localhost', + tls: false } } ); }); }); + describe('parseOptions', () => { + it('should throw error if tls socket option is set to true and the url protocol is "redis:"', () => { + assert.throws( + () => RedisClient.parseOptions({ + url: 'redis://localhost', + socket: { + tls: true + } + }), + TypeError + ); + }); + it('should throw error if tls socket option is set to false and the url protocol is "rediss:"', () => { + assert.throws( + () => RedisClient.parseOptions({ + url: 'rediss://localhost', + socket: { + tls: false + } + }), + TypeError + ); + }); + it('should not throw when tls socket option and url protocol matches"', () => { + assert.equal( + RedisClient.parseOptions({ + url: 'rediss://localhost', + socket: { + tls: true + } + }).socket.tls, + true + ); + assert.equal( + RedisClient.parseOptions({ + url: 'redis://localhost', + socket: { + tls: false + } + }).socket.tls, + false + ); + }); + }); + describe('authentication', () => { testUtils.testWithClient('Client should be authenticated', async client => { assert.equal( diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 128dc599677..ccad872e223 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -315,21 +315,45 @@ export default class RedisClient< return RedisClient.factory(options)(options); } - static parseURL(url: string): RedisClientOptions { + static parseOptions(options: O): O { + if (options?.url) { + const parsed = RedisClient.parseURL(options.url); + if (options.socket) { + if (options.socket.tls !== undefined && options.socket.tls !== parsed.socket.tls) { + throw new TypeError(`tls socket option is set to ${options.socket.tls} which is mismatch with protocol or the URL ${options.url} passed`) + } + parsed.socket = Object.assign(options.socket, parsed.socket); + } + + Object.assign(options, parsed); + } + return options; + } + + static parseURL(url: string): RedisClientOptions & { + socket: Exclude & { + tls: boolean + } + } { // https://www.iana.org/assignments/uri-schemes/prov/redis const { hostname, port, protocol, username, password, pathname } = new URL(url), - parsed: RedisClientOptions = { + parsed: RedisClientOptions & { + socket: Exclude & { + tls: boolean + } + } = { socket: { - host: hostname + host: hostname, + tls: false } }; - if (protocol === 'rediss:') { - parsed!.socket!.tls = true; - } else if (protocol !== 'redis:') { + if (protocol !== 'redis:' && protocol !== 'rediss:') { throw new TypeError('Invalid protocol'); } + parsed.socket.tls = protocol === 'rediss:'; + if (port) { (parsed.socket as TcpSocketConnectOpts).port = Number(port); } @@ -464,15 +488,6 @@ export default class RedisClient< }; } - 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._self.#selectedDB = options.database; } @@ -481,6 +496,10 @@ export default class RedisClient< this._commandOptions = options.commandOptions; } + if (options) { + return RedisClient.parseOptions(options); + } + return options; } From de5e916e05cde15938ef15039ff93c6cf8e558e5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:57:55 +0000 Subject: [PATCH 214/244] Release client@5.8.0 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ecc96aff06..e94583bb6ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.7.0.tgz", + "integrity": "sha512-YV3Knspdj9k6H6s4v8QRcj1WBxHt40vtPmszLKGwRUOUpUOLWSlI9oCUjprMDcQNzgSCXGXYdL/Aj6nT2+Ub0w==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.7.0", diff --git a/packages/client/package.json b/packages/client/package.json index 091ddaf5c91..b7c11d57a84 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 42ec77115169334ba4ecdf4450081e38ca6a2012 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:03 +0000 Subject: [PATCH 215/244] Release bloom@5.8.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e94583bb6ad..11dd6d69ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.7.0.tgz", + "integrity": "sha512-KtBHDH2Aw1BxYDQd87PJsdEmZcpMbD4oPzdBwB4IvSRmMovukO2NNGi5vpCHhCoicS83zu7cjX1fw79uFBZFJA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.7.0" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.7.0.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index edc106214e0..00af3c18690 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" }, "devDependencies": { "@redis/test-utils": "*" From cf16f8b790bb84a5c0b72d9f2c0e324df1ffd184 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:10 +0000 Subject: [PATCH 216/244] Release json@5.8.0 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11dd6d69ce3..f30ee3a7d1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.7.0.tgz", + "integrity": "sha512-VP3wtse1PSB/UjZAV1lWyDrWrrZcwi/cjb3L0lIarcIJ+EbHliB2QPml0Bvjz8F8F0eDJRtChJVXFc+jhGxCtA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.7.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.7.0", diff --git a/packages/json/package.json b/packages/json/package.json index fac6209c360..bab27df34cd 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" }, "devDependencies": { "@redis/test-utils": "*" From 1436a6e304c2c369561e0e0b13626baae212253f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:17 +0000 Subject: [PATCH 217/244] Release search@5.8.0 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f30ee3a7d1e..051afd3243b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.7.0" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.7.0.tgz", + "integrity": "sha512-dDZIq8pZJnT+kZ9xRlLLi2Rvkd792z9eh31QRIwPr5wXjAXeaQ+Nf65em6dLpsxZ60MmhwDwLrBPJpYVjKPBPQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.7.0" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 5d7fffcfd26..8095a418d66 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" }, "devDependencies": { "@redis/test-utils": "*" From 4b6a3d1c39ac88521dc440791ce6c2d7c3b3dc3f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:25 +0000 Subject: [PATCH 218/244] Release time-series@5.8.0 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 051afd3243b..0ae7329f750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.7.0" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.7.0.tgz", + "integrity": "sha512-AJTF9sz3y1MJAukgQW4Jw8zt8qGOE3+1d87pufOP35zsFBlHipGscpctoXiNMebfy0114y/FjSprr65LjbJQSQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.7.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.0", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 44fca5643f3..0ca01943a5a 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" }, "devDependencies": { "@redis/test-utils": "*" From 0916d33b120c41cac892436768b30674c82c0383 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:32 +0000 Subject: [PATCH 219/244] Release entraid@5.8.0 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ae7329f750..4c53f7eea63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index cb57d9074c3..2ef4cfca794 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.7.0" + "@redis/client": "^5.8.0" }, "devDependencies": { "@types/express": "^4.17.21", From 12f7d8a7fef4cb0961afa56f153236356e003e86 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 09:58:40 +0000 Subject: [PATCH 220/244] Release redis@5.8.0 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c53f7eea63..ede6844f0be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.7.0", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.7.0", - "@redis/client": "5.7.0", - "@redis/json": "5.7.0", - "@redis/search": "5.7.0", - "@redis/time-series": "5.7.0" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.7.0.tgz", - "integrity": "sha512-KtBHDH2Aw1BxYDQd87PJsdEmZcpMbD4oPzdBwB4IvSRmMovukO2NNGi5vpCHhCoicS83zu7cjX1fw79uFBZFJA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.7.0" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.7.0.tgz", - "integrity": "sha512-YV3Knspdj9k6H6s4v8QRcj1WBxHt40vtPmszLKGwRUOUpUOLWSlI9oCUjprMDcQNzgSCXGXYdL/Aj6nT2+Ub0w==", + "version": "5.8.0", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.8.0", + "@redis/client": "5.8.0", + "@redis/json": "5.8.0", + "@redis/search": "5.8.0", + "@redis/time-series": "5.8.0" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.7.0.tgz", - "integrity": "sha512-VP3wtse1PSB/UjZAV1lWyDrWrrZcwi/cjb3L0lIarcIJ+EbHliB2QPml0Bvjz8F8F0eDJRtChJVXFc+jhGxCtA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.7.0" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.7.0.tgz", - "integrity": "sha512-dDZIq8pZJnT+kZ9xRlLLi2Rvkd792z9eh31QRIwPr5wXjAXeaQ+Nf65em6dLpsxZ60MmhwDwLrBPJpYVjKPBPQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.7.0" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.7.0.tgz", - "integrity": "sha512-AJTF9sz3y1MJAukgQW4Jw8zt8qGOE3+1d87pufOP35zsFBlHipGscpctoXiNMebfy0114y/FjSprr65LjbJQSQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.7.0" - } - }, "packages/search": { "name": "@redis/search", "version": "5.8.0", diff --git a/packages/redis/package.json b/packages/redis/package.json index a14f6ba10bc..c3cbc01442e 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.7.0", + "version": "5.8.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.7.0", - "@redis/client": "5.7.0", - "@redis/json": "5.7.0", - "@redis/search": "5.7.0", - "@redis/time-series": "5.7.0" + "@redis/bloom": "5.8.0", + "@redis/client": "5.8.0", + "@redis/json": "5.8.0", + "@redis/search": "5.8.0", + "@redis/time-series": "5.8.0" }, "engines": { "node": ">= 18" From 6406172ea8f833cb483206c452b0c4f16958d393 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 6 Aug 2025 09:49:52 +0300 Subject: [PATCH 221/244] chore(tests): bump test container version 8.2 (#3046) --- .github/workflows/tests.yml | 2 +- README.md | 3 ++- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/sentinel/test-util.ts | 32 +++++++++++------------ packages/client/lib/test-utils.ts | 2 +- packages/entraid/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/search/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 9 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89925b55d44..9753535b53d 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.4.0-v1", "8.0.2", "8.2-rc2-pre"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2"] steps: - uses: actions/checkout@v4 with: diff --git a/README.md b/README.md index ab6b4707e6f..9948858db9f 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manua ```typescript // Enable client side caching with RESP3 const client = createClient({ - RESP: 3, + RESP: 3, clientSideCache: { ttl: 0, // Time-to-live (0 = no expiration) maxEntries: 0, // Maximum entries (0 = unlimited) @@ -304,6 +304,7 @@ Node Redis is supported with the following versions of Redis: | Version | Supported | | ------- | ------------------ | +| 8.2.z | :heavy_check_mark: | | 8.0.z | :heavy_check_mark: | | 7.4.z | :heavy_check_mark: | | 7.2.z | :heavy_check_mark: | diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 0f77acae6f6..64bc3484090 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.2-rc2-pre' + defaultDockerVersion: '8.2' }); export const GLOBAL = { diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index a88c9818580..1f8d75a76da 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,16 +174,16 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-rc2-pre' + defaultDockerVersion: '8.2' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); } - getSentinelClient(opts?: Partial>, errors = true) { if (opts?.sentinelRootNodes !== undefined) { throw new Error("cannot specify sentinelRootNodes here"); @@ -252,7 +252,7 @@ export class SentinelFramework extends DockerBase { protected async spawnRedisSentinelNodes(replicasCount: number) { const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) - + const replicas: Array = [] for (let i = 0; i < replicasCount; i++) { const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) @@ -282,7 +282,7 @@ export class SentinelFramework extends DockerBase { async getAllRunning() { for (const port of this.getAllNodesPort()) { let first = true; - while (await isPortAvailable(port)) { + while (await isPortAvailable(port)) { if (!first) { console.log(`problematic restart ${port}`); await setTimeout(500); @@ -295,7 +295,7 @@ export class SentinelFramework extends DockerBase { for (const port of this.getAllSentinelsPort()) { let first = true; - while (await isPortAvailable(port)) { + while (await isPortAvailable(port)) { if (!first) { await setTimeout(500); } else { @@ -325,7 +325,7 @@ export class SentinelFramework extends DockerBase { await client.connect(); await client.replicaOf("127.0.0.1", masterPort); await client.close(); - + this.#nodeList.push(replica); this.#nodeMap.set(replica.port.toString(), replica); @@ -333,9 +333,9 @@ export class SentinelFramework extends DockerBase { async getMaster(tracer?: Array): Promise { const client = RedisClient.create({ - name: this.config.sentinelName, + name: this.config.sentinelName, socket: { - host: "127.0.0.1", + host: "127.0.0.1", port: this.#sentinelList[0].port, }, modules: RedisSentinelModule, @@ -464,9 +464,9 @@ export class SentinelFramework extends DockerBase { async sentinelSentinels() { const client = RedisClient.create({ - name: this.config.sentinelName, + name: this.config.sentinelName, socket: { - host: "127.0.0.1", + host: "127.0.0.1", port: this.#sentinelList[0].port, }, modules: RedisSentinelModule, @@ -480,9 +480,9 @@ export class SentinelFramework extends DockerBase { async sentinelMaster() { const client = RedisClient.create({ - name: this.config.sentinelName, + name: this.config.sentinelName, socket: { - host: "127.0.0.1", + host: "127.0.0.1", port: this.#sentinelList[0].port, }, modules: RedisSentinelModule, @@ -496,9 +496,9 @@ export class SentinelFramework extends DockerBase { async sentinelReplicas() { const client = RedisClient.create({ - name: this.config.sentinelName, + name: this.config.sentinelName, socket: { - host: "127.0.0.1", + host: "127.0.0.1", port: this.#sentinelList[0].port, }, modules: RedisSentinelModule, @@ -509,4 +509,4 @@ export class SentinelFramework extends DockerBase { return replicas } -} \ No newline at end of file +} diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 62509dee143..d6cb67aa01d 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-rc2-pre' + defaultDockerVersion: '8.2' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 1b63a955bfa..2a240db3418 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.2-rc2-pre' + defaultDockerVersion: '8.2' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 81a546fcd64..629c2a5fd60 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.2-rc2-pre' + defaultDockerVersion: '8.2' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index ef4c759b43a..ed1f864ef2e 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.2-rc2-pre' + defaultDockerVersion: '8.2' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 388069ca8a7..d454a3c6b63 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.2-rc2-pre' + defaultDockerVersion: '8.2' }); export const GLOBAL = { From 2f106324507eec905b8fe7691ba11179acdeeca7 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 6 Aug 2025 09:50:19 +0300 Subject: [PATCH 222/244] fix(commands): expire, expireAt are not readonly (#3045) fixes #3044 --- packages/client/lib/commands/EXPIRE.ts | 1 - packages/client/lib/commands/EXPIREAT.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/client/lib/commands/EXPIRE.ts b/packages/client/lib/commands/EXPIRE.ts index 15855832c32..985b81071a3 100644 --- a/packages/client/lib/commands/EXPIRE.ts +++ b/packages/client/lib/commands/EXPIRE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, /** * Sets a timeout on key. After the timeout has expired, the key will be automatically deleted * @param parser - The Redis command parser diff --git a/packages/client/lib/commands/EXPIREAT.ts b/packages/client/lib/commands/EXPIREAT.ts index 4956b8aa23b..a20407aa78e 100644 --- a/packages/client/lib/commands/EXPIREAT.ts +++ b/packages/client/lib/commands/EXPIREAT.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformEXAT } from './generic-transformers'; export default { - IS_READ_ONLY: true, /** * Sets the expiration for a key at a specific Unix timestamp * @param parser - The Redis command parser From 68cebf7f6e05f81cae18629e49a3a17caa8e3802 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Wed, 13 Aug 2025 08:51:30 +0300 Subject: [PATCH 223/244] fix: parse database from Redis URL (#3052) --- packages/client/lib/client/index.spec.ts | 8 ++++++++ packages/client/lib/client/index.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 0aed98450d2..9c33f2ce847 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -167,6 +167,14 @@ describe('Client', () => { } ); }); + + it('DB in URL should be parsed', async () => { + const client = RedisClient.create({ + url: 'redis://user:secret@localhost:6379/5' + }); + + assert.equal(client?.options?.database, 5); + }) }); describe('parseOptions', () => { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index ccad872e223..1a27ea88986 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -496,8 +496,14 @@ export default class RedisClient< this._commandOptions = options.commandOptions; } - if (options) { - return RedisClient.parseOptions(options); + if (options?.url) { + const parsedOptions = RedisClient.parseOptions(options); + + if (parsedOptions?.database) { + this._self.#selectedDB = parsedOptions.database; + } + + return parsedOptions; } return options; From 883375cf4d16f948423e1f7fd778949ec9a08e11 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:15 +0000 Subject: [PATCH 224/244] Release client@5.8.1 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ede6844f0be..b2209c7533b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", + "integrity": "sha512-ywZjKGoSSAECGYOd9bJpws6d4867SN686obUWT/sRmo1c/Q8V+jWyInvlqwKa0BOvTHHwYeB2WFUEvd6PADeOQ==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.0", diff --git a/packages/client/package.json b/packages/client/package.json index b7c11d57a84..c685f60e77b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 4900d2b2ad0328f422318021bd61df4e99874475 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:21 +0000 Subject: [PATCH 225/244] Release bloom@5.8.1 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2209c7533b..bb7986cc951 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.0.tgz", + "integrity": "sha512-kpKZzAAjGiGYn88Bqq6+ozxPg6kGYWRZH9vnOwGcoSCbrW14SZpZVMYMFSio8FH9ZJUdUcmT/RLGlA1W1t0UWQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 00af3c18690..35e65009cef 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*" From 80c3dd3dc23a9e8446e2a54898c62cf04f6aadeb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:27 +0000 Subject: [PATCH 226/244] Release json@5.8.1 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb7986cc951..85852311032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.0.tgz", + "integrity": "sha512-xPBpwY6aKoRzMSu67MpwrBiSliON9bfHo9Y/pSPBjW8/KoOm1MzGqwJUO20qdjXpFoKJsDWwxIE1LHdBNzcImw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.0", diff --git a/packages/json/package.json b/packages/json/package.json index bab27df34cd..2879186a9ca 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*" From 603fa71fe13a6664f3e3ada2e73413fa3ad1cf89 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:33 +0000 Subject: [PATCH 227/244] Release search@5.8.1 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85852311032..70cfac04da1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.8.0" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.0.tgz", + "integrity": "sha512-lF9pNv9vySmirm1EzCH6YeRjhvH6lLT7tvebYHEM7WTkEQ/7kZWb4athliKESHpxzTQ36U9UbzuedSywHV6OhQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 8095a418d66..866371ba118 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*" From c0e6c788732aa6c88435f7a9a6133f23bea40d08 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:39 +0000 Subject: [PATCH 228/244] Release time-series@5.8.1 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70cfac04da1..0abb53badd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.8.0" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.0.tgz", + "integrity": "sha512-kPTlW2ACXokjQNXjCD8Pw9mHDoB94AHUlHFahyjxz9lUJUVwiva2Dgfrd2k3JxHhSBqyY2PREIj9YwIUSTSSqQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.1", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 0ca01943a5a..b22bbd9cb08 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*" From 4e5e31dc554b2c9bf1bbfa23be2dd502da4f4385 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:45 +0000 Subject: [PATCH 229/244] Release entraid@5.8.1 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0abb53badd7..b7a57379ee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 2ef4cfca794..2eee504a1fd 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@types/express": "^4.17.21", From cafdc6345995851d6dab05d6292acb9331083483 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 06:22:51 +0000 Subject: [PATCH 230/244] Release redis@5.8.1 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7a57379ee5..b4fa4834c61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.8.0", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.8.0", - "@redis/client": "5.8.0", - "@redis/json": "5.8.0", - "@redis/search": "5.8.0", - "@redis/time-series": "5.8.0" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.0.tgz", - "integrity": "sha512-kpKZzAAjGiGYn88Bqq6+ozxPg6kGYWRZH9vnOwGcoSCbrW14SZpZVMYMFSio8FH9ZJUdUcmT/RLGlA1W1t0UWQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", - "integrity": "sha512-ywZjKGoSSAECGYOd9bJpws6d4867SN686obUWT/sRmo1c/Q8V+jWyInvlqwKa0BOvTHHwYeB2WFUEvd6PADeOQ==", + "version": "5.8.1", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.8.1", + "@redis/client": "5.8.1", + "@redis/json": "5.8.1", + "@redis/search": "5.8.1", + "@redis/time-series": "5.8.1" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.0.tgz", - "integrity": "sha512-xPBpwY6aKoRzMSu67MpwrBiSliON9bfHo9Y/pSPBjW8/KoOm1MzGqwJUO20qdjXpFoKJsDWwxIE1LHdBNzcImw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.0.tgz", - "integrity": "sha512-lF9pNv9vySmirm1EzCH6YeRjhvH6lLT7tvebYHEM7WTkEQ/7kZWb4athliKESHpxzTQ36U9UbzuedSywHV6OhQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.0.tgz", - "integrity": "sha512-kPTlW2ACXokjQNXjCD8Pw9mHDoB94AHUlHFahyjxz9lUJUVwiva2Dgfrd2k3JxHhSBqyY2PREIj9YwIUSTSSqQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, "packages/search": { "name": "@redis/search", "version": "5.8.1", diff --git a/packages/redis/package.json b/packages/redis/package.json index c3cbc01442e..de4ac275ed0 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.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.8.0", - "@redis/client": "5.8.0", - "@redis/json": "5.8.0", - "@redis/search": "5.8.0", - "@redis/time-series": "5.8.0" + "@redis/bloom": "5.8.1", + "@redis/client": "5.8.1", + "@redis/json": "5.8.1", + "@redis/search": "5.8.1", + "@redis/time-series": "5.8.1" }, "engines": { "node": ">= 18" From 746e9b184b84e98383d7fac47f2a219cbb75d970 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Wed, 13 Aug 2025 15:26:14 -0400 Subject: [PATCH 231/244] docs: Clustering sendCommand docs (#3053) We noticed that `sendCommand()` takes different arguments for clusters vs clients, and I wanted to document the differences. I think I got it correct, but please review closely just to be sure. --- docs/clustering.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/clustering.md b/docs/clustering.md index 6803583fa51..3e4f8446b6a 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -38,6 +38,25 @@ await cluster.close(); | scripts | | Script definitions (see [Lua Scripts](./programmability.md#lua-scripts)) | | functions | | Function definitions (see [Functions](./programmability.md#functions)) | +## Usage + +Most redis commands are the same as with individual clients. + +### Unsupported Redis Commands + +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`. + +When clustering, `sendCommand` takes 3 arguments to help with routing to the correct redis node: +* `firstKey`: the key that is being operated on, or `undefined` to route to a random node. +* `isReadOnly`: determines if the command needs to go to the master or may go to a replica. +* `args`: the command and all arguments (including the key), as an array of strings. + +```javascript +await cluster.sendCommand("key", false, ["SET", "key", "value", "NX"]); // 'OK' + +await cluster.sendCommand("key", true, ["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] +``` + ## 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. @@ -114,3 +133,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. The client sends these commands to a random node in order to spread the load across the cluster. + From 82847fb92c07b86ba72eba364ce879e0249009d5 Mon Sep 17 00:00:00 2001 From: Arek W Date: Thu, 14 Aug 2025 09:42:36 +0200 Subject: [PATCH 232/244] fix: Stop erasing `ErrorReply` stack (#3050) It was very difficult to debug `ErrorReply` errors due to `error.stack` being erased. Given this restores standard JS Error behaviour, I have not added any tests. --- packages/client/lib/errors.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index db37ec1a9ba..5cb9166df02 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -63,12 +63,7 @@ export class ReconnectStrategyError extends Error { } } -export class ErrorReply extends Error { - constructor(message: string) { - super(message); - this.stack = undefined; - } -} +export class ErrorReply extends Error {} export class SimpleError extends ErrorReply {} From fceb60968e3ebc36dbb457a24b8cbb1ca0db01ae Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Thu, 14 Aug 2025 03:49:05 -0400 Subject: [PATCH 233/244] docs: Call out sendCommand cluster difference in readme (#3054) A followup to https://github.com/redis/node-redis/pull/3053 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9948858db9f..05c55985b33 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK' await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] ``` +_Note: the [API is different when using a cluster](https://github.com/redis/node-redis/blob/master/docs/clustering.md#unsupported-redis-commands)._ + ### Transactions (Multi/Exec) Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When From ed6aca7d03339a84515b2cf30b17161bcdb375f5 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Mon, 18 Aug 2025 10:22:44 +0200 Subject: [PATCH 234/244] fix(ts): xtrim threshold accepts string (#3058) * fix(ts): xtrim threshold accepts string * test: check MINID with text id --- packages/client/lib/commands/XTRIM.spec.ts | 15 +++++++++++++++ packages/client/lib/commands/XTRIM.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index b88cf84676e..38254d565e9 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -18,6 +18,11 @@ describe('XTRIM', () => { parseArgs(XTRIM, 'key', 'MINID', 123), ['XTRIM', 'key', 'MINID', '123'] ); + + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MINID', '0-0'), + ['XTRIM', 'key', 'MINID', '0-0'] + ); }); it('with strategyModifier', () => { @@ -89,6 +94,16 @@ describe('XTRIM', () => { cluster: GLOBAL.CLUSTERS.OPEN, }); + testUtils.testAll('xTrim with string MINID', async client => { + assert.equal( + typeof await client.xTrim('key', 'MINID', '0-0'), + 'number' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + }); + testUtils.testAll( 'xTrim with LIMIT', async (client) => { diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 34171d4611e..8d40824d791 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -37,7 +37,7 @@ export default { parser: CommandParser, key: RedisArgument, strategy: 'MAXLEN' | 'MINID', - threshold: number, + threshold: number | string, options?: XTrimOptions ) { parser.push('XTRIM') From d5423b93d639a8f8e8e612e49034c01e69639cb9 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 18 Aug 2025 11:32:41 +0300 Subject: [PATCH 235/244] chore(tests): bump test container version 8.2.1-pre (#3057) --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/sentinel/test-util.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/test-utils.ts | 2 +- packages/test-utils/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9753535b53d..f522282d28d 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.4.0-v1", "8.0.2", "8.2"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2", "8.2.1-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 64bc3484090..c2991de60d0 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.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 1f8d75a76da..33e8e330c25 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index d6cb67aa01d..e54a7d7647c 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 2a240db3418..7a624902f5a 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.2' + defaultDockerVersion: '8.2.1-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 629c2a5fd60..2cc3804fe90 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.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index ed1f864ef2e..9b82816cd4f 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.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/test-utils/lib/test-utils.ts b/packages/test-utils/lib/test-utils.ts index fe27bd93d37..11364493091 100644 --- a/packages/test-utils/lib/test-utils.ts +++ b/packages/test-utils/lib/test-utils.ts @@ -3,7 +3,7 @@ import TestUtils from './index' export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2.1-pre' }); diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index d454a3c6b63..2c345961630 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.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { From e347d566cb824c9945d8fe86bd80cc69eeb0195c Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 20 Aug 2025 11:11:34 +0300 Subject: [PATCH 236/244] fix(sentinel): properly pass reconnectStrategy (#3063) Properly pass reconnectStrategy to master/replica nodes. Before that strategies passed in the nodeClientOptions.socket object were ignored. fixes #3061 --- packages/client/lib/sentinel/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index b3f3bbf0b8d..63c45862935 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -716,7 +716,7 @@ class RedisSentinelInternal< ); } - #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) { + #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: false) { return RedisClient.create({ //first take the globally set RESP RESP: this.#RESP, @@ -726,7 +726,7 @@ class RedisSentinelInternal< ...clientOptions.socket, host: node.host, port: node.port, - reconnectStrategy + ...(reconnectStrategy !== undefined && { reconnectStrategy }) } }); } From b9a5d3664087231ae0bb006af4ae040874735c95 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 20 Aug 2025 11:12:25 +0300 Subject: [PATCH 237/244] fix(search): properly decide if response has docs (#3060) fixes: #3056 --- packages/search/lib/commands/SEARCH.spec.ts | 79 +++++++++++++++++++++ packages/search/lib/commands/SEARCH.ts | 3 +- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index ab480808ffa..97e1a9a9885 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -326,5 +326,84 @@ describe('FT.SEARCH', () => { } ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('properly parse content/nocontent scenarios', async client => { + + const indexName = 'foo'; + await client.ft.create( + indexName, + { + itemOrder: { + type: 'NUMERIC', + SORTABLE: true, + }, + name: { + type: 'TEXT', + }, + }, + { + ON: 'HASH', + PREFIX: 'item:', + } + ); + + await client.hSet("item:1", { + itemOrder: 1, + name: "First item", + }); + + await client.hSet("item:2", { + itemOrder: 2, + name: "Second item", + }); + + await client.hSet("item:3", { + itemOrder: 3, + name: "Third item", + }); + + // Search with SORTBY and LIMIT + let result = await client.ft.search(indexName, "@itemOrder:[0 10]", { + SORTBY: { + BY: "itemOrder", + DIRECTION: "ASC", + }, + LIMIT: { + from: 0, + size: 1, // only get first result + }, + }); + + assert.equal(result.total, 3, "Result's `total` value reflects the total scanned documents"); + assert.equal(result.documents.length, 1); + let doc = result.documents[0]; + assert.equal(doc.id, 'item:1'); + assert.equal(doc.value.itemOrder, '1'); + assert.equal(doc.value.name, 'First item'); + + await client.del("item:3"); + + // Search again after removing item:3 + result = await client.ft.search(indexName, "@itemOrder:[0 10]", { + SORTBY: { + BY: "itemOrder", + DIRECTION: "ASC", + }, + LIMIT: { + from: 0, + size: 1, // only get first result + }, + }); + + assert.equal(result.total, 2, "Result's `total` value reflects the total scanned documents"); + assert.equal(result.documents.length, 1); + doc = result.documents[0]; + assert.equal(doc.id, 'item:1'); + assert.equal(doc.value.itemOrder, '1'); + assert.equal(doc.value.name, 'First item'); + + + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 61e1d8d84d2..03779a446cc 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -183,7 +183,8 @@ export default { }, transformReply: { 2: (reply: SearchRawReply): SearchReply => { - const withoutDocuments = (reply[0] + 1 == reply.length) + // if reply[2] is array, then we have content/documents. Otherwise, only ids + const withoutDocuments = reply.length > 2 && !Array.isArray(reply[2]); const documents = []; let i = 1; From 2e660120996d29ca7e4093e1fafdb03bb6374618 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:04 +0000 Subject: [PATCH 238/244] Release client@5.8.2 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4fa4834c61..f99a8dbf5e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", + "integrity": "sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.1", diff --git a/packages/client/package.json b/packages/client/package.json index c685f60e77b..1332083bf18 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 0a5d076cf511204da8ac8b87601e4298ea731b97 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:10 +0000 Subject: [PATCH 239/244] Release bloom@5.8.2 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f99a8dbf5e7..3c45f421cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.1.tgz", + "integrity": "sha512-hJOJr/yX6BttnyZ+nxD3Ddiu2lPig4XJjyAK1v7OSHOJNUTfn3RHBryB9wgnBMBdkg9glVh2AjItxIXmr600MA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.1" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 35e65009cef..e2ff5a8b42d 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" From 5e575b9550e8f970c364189026b2e44e966ccd74 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:16 +0000 Subject: [PATCH 240/244] Release json@5.8.2 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c45f421cf9..c2a56173c62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.1.tgz", + "integrity": "sha512-kyvM8Vn+WjJI++nRsIoI9TbdfCs1/TgD0Hp7Z7GiG6W4IEBzkXGQakli+R5BoJzUfgh7gED2fkncYy1NLprMNg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.1", diff --git a/packages/json/package.json b/packages/json/package.json index 2879186a9ca..ff689dd17ee 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" From c73a8c6569e5031ec545f1124e6d502e5a772528 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:23 +0000 Subject: [PATCH 241/244] Release search@5.8.2 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2a56173c62..71e161933f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.8.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.1.tgz", + "integrity": "sha512-CzuKNTInTNQkxqehSn7QiYcM+th+fhjQn5ilTvksP1wPjpxqK0qWt92oYg3XZc3tO2WuXkqDvTujc4D7kb6r/A==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 866371ba118..40238080e8b 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" From 01157a014767d79d0808c069e21ebcc0c423f33b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:29 +0000 Subject: [PATCH 242/244] Release time-series@5.8.2 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71e161933f4..abcc27d1bda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.8.1" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.1.tgz", + "integrity": "sha512-klvdR96U9oSOyqvcectoAGhYlMOnMS3I5UWUOgdBn1buMODiwM/E4Eds7gxldKmtowe4rLJSF1CyIqyZTjy8Ow==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.8.2", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index b22bbd9cb08..46ea5b16fef 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" From 3d6eefc2469091fad057cc28670bf5562d869626 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:35 +0000 Subject: [PATCH 243/244] Release entraid@5.8.2 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index abcc27d1bda..71ce9570246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 2eee504a1fd..9991fa3fb89 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@types/express": "^4.17.21", From 7b56e9a3dca9222d04b4b3741ab209f9f0adaf09 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Aug 2025 08:33:41 +0000 Subject: [PATCH 244/244] Release redis@5.8.2 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ce9570246..736abe70a46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.8.1", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.8.1", - "@redis/client": "5.8.1", - "@redis/json": "5.8.1", - "@redis/search": "5.8.1", - "@redis/time-series": "5.8.1" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.1.tgz", - "integrity": "sha512-hJOJr/yX6BttnyZ+nxD3Ddiu2lPig4XJjyAK1v7OSHOJNUTfn3RHBryB9wgnBMBdkg9glVh2AjItxIXmr600MA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", - "integrity": "sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==", + "version": "5.8.2", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.8.2", + "@redis/client": "5.8.2", + "@redis/json": "5.8.2", + "@redis/search": "5.8.2", + "@redis/time-series": "5.8.2" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.1.tgz", - "integrity": "sha512-kyvM8Vn+WjJI++nRsIoI9TbdfCs1/TgD0Hp7Z7GiG6W4IEBzkXGQakli+R5BoJzUfgh7gED2fkncYy1NLprMNg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.1.tgz", - "integrity": "sha512-CzuKNTInTNQkxqehSn7QiYcM+th+fhjQn5ilTvksP1wPjpxqK0qWt92oYg3XZc3tO2WuXkqDvTujc4D7kb6r/A==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.1.tgz", - "integrity": "sha512-klvdR96U9oSOyqvcectoAGhYlMOnMS3I5UWUOgdBn1buMODiwM/E4Eds7gxldKmtowe4rLJSF1CyIqyZTjy8Ow==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.8.2", diff --git a/packages/redis/package.json b/packages/redis/package.json index de4ac275ed0..583a6606817 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.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.8.1", - "@redis/client": "5.8.1", - "@redis/json": "5.8.1", - "@redis/search": "5.8.1", - "@redis/time-series": "5.8.1" + "@redis/bloom": "5.8.2", + "@redis/client": "5.8.2", + "@redis/json": "5.8.2", + "@redis/search": "5.8.2", + "@redis/time-series": "5.8.2" }, "engines": { "node": ">= 18"