diff --git a/.github/release-drafter/graph-config.yml b/.github/release-drafter-base.yml similarity index 88% rename from .github/release-drafter/graph-config.yml rename to .github/release-drafter-base.yml index 88d76b78b55..ea259fc0d2d 100644 --- a/.github/release-drafter/graph-config.yml +++ b/.github/release-drafter-base.yml @@ -1,5 +1,5 @@ -name-template: 'graph@$NEXT_PATCH_VERSION' -tag-template: 'graph@$NEXT_PATCH_VERSION' +name-template: 'json@$NEXT_PATCH_VERSION' +tag-template: 'json@$NEXT_PATCH_VERSION' autolabeler: - label: 'chore' files: @@ -33,9 +33,10 @@ categories: - 'maintenance' - 'documentation' - 'docs' + change-template: '- $TITLE (#$NUMBER)' include-paths: - - 'packages/graph' + - 'packages/json' exclude-labels: - 'skip-changelog' template: | 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/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/release-drafter-graph.yml b/.github/workflows/release-drafter-entraid.yml similarity index 90% rename from .github/workflows/release-drafter-graph.yml rename to .github/workflows/release-drafter-entraid.yml index 4d664e5f19e..d522c6cef6f 100644 --- a/.github/workflows/release-drafter-graph.yml +++ b/.github/workflows/release-drafter-entraid.yml @@ -19,6 +19,6 @@ jobs: - 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 + config-name: release-drafter/entraid-config.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84d70d6b4c0..4ad7883a262 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,25 +5,30 @@ on: branches: - 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'] - redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2'] + node-version: [ '18', '20', '22' ] + redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-RC2-pre' ] 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 @@ -31,10 +36,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/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! diff --git a/README.md b/README.md index a590372b1b1..ac6394461f1 100644 --- a/README.md +++ b/README.md @@ -25,26 +25,12 @@ 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: -``` bash -docker run -p 6379:6379 -it redis/redis-stack-server:latest +```bash +docker run -p 6379:6379 -d redis:8.0-rc1 ``` To install node-redis, simply: @@ -52,77 +38,99 @@ 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`](./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 | -> :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)! -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'; +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.disconnect(); +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]`: +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' + 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). +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.): ```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: ```typescript -await client.set('key', 'value', { +await client.set("key", "value", { EX: 10, - NX: true + 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'] +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: } +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 @@ -130,50 +138,53 @@ await client.hGetAll( 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(["SET", "key", "value", "NX"]); // 'OK' -await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2'] +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: +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'); +await client.set("another-key", "another-value"); const [setKeyReply, otherKeyValue] = await client .multi() - .set('key', 'value') - .get('another-key') + .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. +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. +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: -This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`: +```javascript +const client = await createClient() + .on("error", (err) => console.error(err)) + .connect(); -```typescript -import { commandOptions } from 'redis'; +await client.ping(client.commandOptions({ isolated: true })); +``` -const blPopPromise = client.blPop( - commandOptions({ isolated: true }), - 'key', - 0 -); +In v5 we've extracted this pool logic into its own class—`RedisClientPool`: -await client.lPush('key', ['1', '2']); +```javascript +const pool = await createClientPool() + .on("error", (err) => console.error(err)) + .connect(); -await blPopPromise; // '2' +await pool.ping(); ``` -To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md). ### Pub/Sub @@ -181,7 +192,8 @@ 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): +[`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()) { @@ -193,129 +205,34 @@ for await (const key of client.scanIterator()) { 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')) {} +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 + 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 -} -``` - -#### `.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. -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. +`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to +`client.destroy()`. ```typescript -await client.disconnect(); +client.destroy(); ``` ### Auto-Pipelining @@ -323,19 +240,25 @@ await client.disconnect(); 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=='); +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()`. ```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=="), ]); ``` +### 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. @@ -344,16 +267,17 @@ Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to 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) | +| 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. +> :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. @@ -362,15 +286,20 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each 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: | +| ------- | ------------------ | +| 8.0.z | :heavy_check_mark: | +| 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. +## 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). 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..f8c2388226b --- /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` +- 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..0564794ac46 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 | | 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 @@ -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; + } } }); ``` @@ -73,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. + 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..56eb048ca0c --- /dev/null +++ b/docs/programmability.md @@ -0,0 +1,87 @@ +# [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 { 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, + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) + }, + 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 { 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: { + add: defineScript({ + SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];', + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 1, + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) + }, + 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..f10b2953df5 --- /dev/null +++ b/docs/sentinel.md @@ -0,0 +1,103 @@ +# 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. | +| 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. + +```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(); +}); +``` + +`.acquire()` +```javascript +const clientLease = await sentinel.acquire(); + +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..fbe02e7c4d6 --- /dev/null +++ b/docs/v4-to-v5.md @@ -0,0 +1,245 @@ +# 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. + +- `RedisFlushModes` -> `REDIS_FLUSH_MODES` [^enum-to-constants] + +## 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` + +### 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). +- `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 + +- `TS.ADD`: `boolean` -> `number` [^boolean-to-number] +- `TS.[M][REV]RANGE`: the `ALIGN` argument has been moved into `AGGREGATION` +- `TS.SYNUPDATE`: `Array>` -> `Record>` +- `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). diff --git a/docs/v5.md b/docs/v5.md new file mode 100644 index 00000000000..a3d0ab68389 --- /dev/null +++ b/docs/v5.md @@ -0,0 +1,91 @@ +# RESP3 Support + +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 +import { createClient, RESP_TYPES } from 'redis'; + +// By default +await client.hGetAll('key'); // Record + +// Use Map instead of plain object +await client.withTypeMapping({ + [RESP_TYPES.MAP]: Map +}).hGetAll('key'); // Map + +// Use both Map and Buffer +await client.withTypeMapping({ + [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 + +[Sentinel](./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..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... -await client.quit(); +client.close(); ``` diff --git a/examples/blocking-list-pop.js b/examples/blocking-list-pop.js index 099c73a2a96..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 ); @@ -27,4 +26,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..356304722c0 100644 --- a/examples/command-with-modifiers.js +++ b/examples/command-with-modifiers.js @@ -8,18 +8,24 @@ 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/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/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..f464fd38be1 100644 --- a/examples/dump-and-restore.js +++ b/examples/dump-and-restore.js @@ -1,22 +1,32 @@ // 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(); + +// 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( - 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..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); -await client.quit(); +// 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 4ac9b575f96..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); } -await client.quit(); +client.close(); diff --git a/examples/lua-multi-incr.js b/examples/lua-multi-incr.js index 8eb1092c295..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()); + }, }), }, }); @@ -24,4 +26,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..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.`); -await client.quit(); +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 65ba1442f7e..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": "../" + "redis": "../packages/redis" } } diff --git a/examples/search-hashes.js b/examples/search-hashes.js index 2f8b5fbf7b6..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' @@ -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..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' }] @@ -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..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' @@ -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..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); + } } -await client.quit(); +client.close(); diff --git a/examples/sorted-set.js b/examples/sorted-set.js index eb1f82867c1..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); + } } -await client.quit(); +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/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..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); } -await client.quit(); +client.close(); diff --git a/examples/topk.js b/examples/topk.js index 35cdc4a8500..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}.`); -await client.quit(); +client.close(); diff --git a/examples/transaction-with-arbitrary-commands.js b/examples/transaction-with-arbitrary-commands.js index 274a362d57e..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']); -await client.quit(); +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() 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..0b8ca6ee37e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,45 +1,31 @@ { - "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", + "ts-node": "^10.9.2", + "tsx": "^4.7.0", + "typedoc": "^0.25.7", + "typescript": "^5.3.3" } }, "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" @@ -48,13 +34,243 @@ "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", + "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.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", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -63,9 +279,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" }, @@ -75,9 +290,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", @@ -89,54 +303,68 @@ }, "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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 +378,17 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, "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", "dev": true, + "license": "MIT", "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 +398,13 @@ } }, "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", "dev": true, + "license": "MIT", "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,38 +412,21 @@ "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", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "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", "dev": true, + "license": "MIT", "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" @@ -220,9 +434,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" }, @@ -232,9 +445,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" }, @@ -243,10 +455,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", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -263,9 +474,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" }, @@ -275,9 +485,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" }, @@ -286,51 +495,46 @@ } }, "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", "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.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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -342,9 +546,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" }, @@ -354,9 +557,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", @@ -368,33 +570,48 @@ }, "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "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", "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -403,57 +620,45 @@ } }, "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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": { @@ -465,6 +670,7 @@ "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" }, @@ -477,179 +683,119 @@ "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", "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "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/linux-x64": { + "version": "0.19.12", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "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/@iarna/toml": { + "version": "2.2.5", "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } + "license": "ISC" }, - "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", "dev": true, + "license": "ISC", "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" + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, - "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/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "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/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", "dev": true, + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=8" } }, - "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/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", "dev": true, - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "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/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", "dev": true, + "license": "MIT", "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" + "p-locate": "^4.1.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==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", "dev": true, + "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "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==", - "dev": true, + "p-try": "^2.0.0" + }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "p-limit": "^2.2.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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==", - "dev": true, "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, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2" }, @@ -662,18 +808,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", @@ -685,52 +829,49 @@ }, "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.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", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "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", "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": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -741,18 +882,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" @@ -762,210 +901,160 @@ } }, "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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==", - "dev": true + "version": "19.1.0", + "dev": true, + "license": "MIT" }, "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", + "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@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" }, @@ -975,15 +1064,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", @@ -1001,8 +1088,8 @@ "resolved": "packages/client", "link": true }, - "node_modules/@redis/graph": { - "resolved": "packages/graph", + "node_modules/@redis/entraid": { + "resolved": "packages/entraid", "link": true }, "node_modules/@redis/json": { @@ -1023,9 +1110,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" }, @@ -1033,29 +1119,37 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", "dev": true, + "license": "BSD-3-Clause", "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==", + "version": "11.2.2", "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", @@ -1064,24 +1158,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" }, @@ -1091,443 +1182,232 @@ }, "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/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "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 + "dev": true, + "license": "MIT" }, "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 + "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 + "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.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 + "version": "4.0.4", + "dev": true, + "license": "MIT" }, - "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/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.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", - "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", - "dev": true + "version": "10.0.6", + "dev": true, + "license": "MIT" }, "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 + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "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/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/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": "10.0.16", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", - "integrity": "sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==", + "version": "17.0.3", "dev": true, + "license": "MIT", "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 + "version": "8.1.5", + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.32", "dev": true, + "license": "MIT", "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 - } - } + "version": "21.0.3", + "dev": true, + "license": "MIT" }, - "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==", + "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": { - "yallist": "^4.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "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==", + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "MIT", "bin": { - "semver": "bin/semver.js" + "acorn": "bin/acorn" }, "engines": { - "node": ">=10" + "node": ">=0.4.0" } }, - "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==", + "node_modules/acorn-walk": { + "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": { - "@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" - } - }, - "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/@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/@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==", - "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" - }, - "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" - } - }, - "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==", - "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" - }, - "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==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=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==", - "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" - } - }, - "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==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "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" - } - }, - "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" + "acorn": "^8.11.0" }, "engines": { "node": ">=0.4.0" } }, - "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==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "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/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" }, @@ -1537,9 +1417,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" @@ -1548,45 +1427,26 @@ "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", - "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" }, @@ -1597,26 +1457,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "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", - "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" }, @@ -1629,9 +1497,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" @@ -1642,9 +1509,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" }, @@ -1654,40 +1520,47 @@ }, "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/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "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.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", "dev": true, + "license": "MIT", "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" } }, + "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", - "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" }, @@ -1697,18 +1570,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", @@ -1724,17 +1595,17 @@ } }, "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", "dev": true, + "license": "MIT", "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": { @@ -1746,9 +1617,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" }, @@ -1757,25 +1627,22 @@ } }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true + "version": "3.2.5", + "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.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1785,14 +1652,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": [ { @@ -1807,57 +1671,86 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "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", "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 - }, - "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" - } + "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": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "version": "4.1.0", "dev": true, + "license": "MIT", "dependencies": { - "buffer": "^6.0.3", + "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", + "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", - "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", @@ -1877,9 +1770,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" }, @@ -1889,9 +1781,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" }, @@ -1899,18 +1790,38 @@ "url": "https://github.com/chalk/ansi-styles?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", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "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==", + "node_modules/boxen/node_modules/camelcase": { + "version": "7.0.1", "dev": true, - "dependencies": { + "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" @@ -1924,9 +1835,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" }, @@ -1939,9 +1849,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" }, @@ -1951,9 +1860,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", @@ -1966,23 +1874,10 @@ "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", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1990,9 +1885,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" }, @@ -2002,14 +1896,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.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.3", "dev": true, "funding": [ { @@ -2025,11 +1916,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "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 +1931,7 @@ } }, "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", "dev": true, "funding": [ { @@ -2057,48 +1947,55 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "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/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": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, + "version": "4.1.0", + "license": "MIT", "dependencies": { - "run-applescript": "^5.0.0" + "run-applescript": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "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", - "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.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", + "version": "10.2.14", "dev": true, + "license": "MIT", "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,11 +2007,21 @@ "node": ">=14.16" } }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "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", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", "dev": true, + "license": "MIT", "dependencies": { "hasha": "^5.0.0", "make-dir": "^3.0.0", @@ -2126,43 +2033,56 @@ } }, "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.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.1", - "get-intrinsic": "^1.0.2" + "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", - "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": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "version": "5.3.1", "dev": true, + "license": "MIT", "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", "dev": true, "funding": [ { @@ -2177,30 +2097,42 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "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", "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chardet": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true + "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": [ { @@ -2208,6 +2140,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2225,9 +2158,7 @@ } }, "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", "dev": true, "funding": [ { @@ -2235,24 +2166,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" }, @@ -2262,9 +2192,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" }, @@ -2273,10 +2202,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", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -2286,32 +2214,26 @@ }, "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": "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", "dev": true, + "license": "ISC", "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": { "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", @@ -2326,26 +2248,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" }, @@ -2355,36 +2274,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.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "11.1.0", "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" @@ -2392,15 +2306,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", @@ -2415,41 +2327,87 @@ "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", - "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/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": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "9.0.0", "dev": true, + "license": "MIT", "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 + "dev": true, + "license": "MIT" }, "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", @@ -2461,9 +2419,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" }, @@ -2476,9 +2433,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" }, @@ -2488,18 +2444,15 @@ }, "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" }, @@ -2512,20 +2465,22 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "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" }, @@ -2538,9 +2493,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" }, @@ -2550,48 +2504,31 @@ }, "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/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==", - "dev": true, + "version": "5.2.1", + "license": "MIT", "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==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, + "version": "5.0.0", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2599,9 +2536,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" }, @@ -2614,9 +2550,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" }, @@ -2626,32 +2561,33 @@ }, "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.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.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": { "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" }, @@ -2661,9 +2597,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", @@ -2678,9 +2613,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", @@ -2690,114 +2624,164 @@ "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", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true + "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", - "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/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/dot-prop": { + "version": "6.0.1", "dev": true, + "license": "MIT", "dependencies": { - "path-type": "^4.0.0" + "is-obj": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=6.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "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==", + "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": { - "is-obj": "^2.0.0" + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, "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 - }, - "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==", - "dev": true + "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, + "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/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, + "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.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", - "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "version": "1.22.3", "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.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 +2791,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 +2805,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" @@ -2832,15 +2816,31 @@ }, "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-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, + "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", @@ -2857,14 +2857,13 @@ } }, "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==", + "version": "2.0.2", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -2872,9 +2871,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", @@ -2889,24 +2887,58 @@ }, "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", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "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/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" }, @@ -2914,13 +2946,19 @@ "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": "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==", + "version": "4.0.0", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2928,9 +2966,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", @@ -2947,310 +2984,226 @@ "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" - }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", "bin": { - "eslint": "bin/eslint.js" + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=4" } }, - "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==", + "node_modules/estraverse": { + "version": "5.3.0", "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=4.0" } }, - "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==", + "node_modules/esutils": { + "version": "2.0.3", "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=0.10.0" } }, - "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==", + "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, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 0.6" } }, - "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, + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.x" } }, - "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/execa": { + "version": "8.0.1", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "cross-spawn": "^7.0.3", + "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": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=10" + "node": ">=16.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "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/execa/node_modules/is-stream": { + "version": "3.0.0", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "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/execa/node_modules/signal-exit": { + "version": "4.1.0", "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "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/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": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" + "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": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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/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": { - "has-flag": "^4.0.0" + "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": ">=8" + "node": ">= 0.8.0" } }, - "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==", + "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, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 0.6" } }, - "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==", + "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, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } + "license": "MIT" }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" + "ms": "2.0.0" } }, - "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==", + "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, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } + "license": "MIT" }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "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, - "engines": { - "node": ">=4.0" + "license": "MIT", + "dependencies": { + "ms": "2.0.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==", + "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, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "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==", + "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, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.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", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } + "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": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -3260,17 +3213,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", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3282,31 +3228,16 @@ "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", "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": [ { @@ -3318,6 +3249,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -3328,9 +3260,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" @@ -3342,32 +3273,40 @@ "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", "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" + "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": "^10.12.0 || >=12.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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", @@ -3382,9 +3321,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" }, @@ -3392,11 +3330,46 @@ "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", - "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", @@ -3410,61 +3383,40 @@ } }, "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", "dev": true, + "license": "MIT", "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": { "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/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", - "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" @@ -3475,18 +3427,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" }, @@ -3494,10 +3444,28 @@ "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", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true, "funding": [ { @@ -3512,13 +3480,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "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", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3530,35 +3498,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.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "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", @@ -3574,49 +3528,57 @@ }, "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/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", - "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", + "dev": true, + "license": "MIT", + "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.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "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", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3624,20 +3586,18 @@ }, "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": "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3645,9 +3605,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" @@ -3659,14 +3618,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "dev": true, + "license": "MIT", + "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", "dev": true, + "license": "MIT", "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,19 +3644,17 @@ } }, "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", "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", @@ -3699,27 +3666,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.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", "dev": true, + "license": "MIT", "dependencies": { "async": "^3.2.4", "commander": "^11.0.0", @@ -3739,28 +3703,25 @@ }, "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" } }, "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", "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", @@ -3778,9 +3739,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" }, @@ -3790,9 +3750,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" }, @@ -3804,37 +3763,17 @@ } }, "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==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "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==", + "version": "11.12.0", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "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" }, @@ -3847,9 +3786,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", @@ -3862,12 +3800,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3875,9 +3814,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", @@ -3898,55 +3836,46 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "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==", + "node_modules/got/node_modules/get-stream": { + "version": "6.0.1", "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.4.0" + "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", - "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": "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", "dev": true, + "license": "MIT", "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.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.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3954,9 +3883,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" }, @@ -3965,10 +3893,11 @@ } }, "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==", + "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": { "node": ">= 0.4" }, @@ -3977,12 +3906,11 @@ } }, "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", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3991,23 +3919,10 @@ "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", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" @@ -4019,53 +3934,57 @@ "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "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": { "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-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", - "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" @@ -4075,10 +3994,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", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -4089,9 +4007,7 @@ }, "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" @@ -4101,19 +4017,17 @@ } }, "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", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=14.18.0" + "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" }, @@ -4123,8 +4037,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": [ { @@ -4139,22 +4051,21 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "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", "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" @@ -4166,38 +4077,42 @@ "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", - "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" @@ -4205,26 +4120,23 @@ }, "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.10", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.10.tgz", - "integrity": "sha512-tVVNFIXU8qNHoULiazz612GFl+yqNfjMTbLuViNJE/d860Qxrd3NMrse8dm40VUQLOQeULvaQF8lpAhvysjeyA==", + "version": "9.2.12", "dev": true, + "license": "MIT", "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,108 +4156,29 @@ "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", "dev": true, + "license": "MIT", "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": { "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/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", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -4366,9 +4199,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" @@ -4382,9 +4214,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" }, @@ -4393,13 +4224,12 @@ } }, "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", "dev": true, + "license": "MIT", "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": { @@ -4408,24 +4238,31 @@ }, "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/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", - "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" @@ -4438,14 +4275,15 @@ } }, "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", "dev": true, + "license": "MIT", "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" @@ -4453,15 +4291,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" }, @@ -4471,9 +4307,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" }, @@ -4483,9 +4318,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" @@ -4499,9 +4333,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" }, @@ -4511,9 +4344,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" }, @@ -4522,12 +4354,11 @@ } }, "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", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4535,9 +4366,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" }, @@ -4550,9 +4380,7 @@ }, "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" }, @@ -4565,27 +4393,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" }, @@ -4593,11 +4418,23 @@ "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", - "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" }, @@ -4613,9 +4450,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" @@ -4629,9 +4465,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" }, @@ -4641,18 +4476,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" }, @@ -4662,9 +4495,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" }, @@ -4674,18 +4506,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" }, @@ -4698,45 +4528,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-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", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4750,18 +4567,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" }, @@ -4771,20 +4586,18 @@ }, "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": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "version": "2.0.1", "dev": true, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4792,9 +4605,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" }, @@ -4807,9 +4619,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" }, @@ -4821,12 +4632,11 @@ } }, "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", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4837,17 +4647,15 @@ }, "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": "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4855,9 +4663,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" }, @@ -4867,66 +4674,39 @@ }, "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": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, + "version": "3.1.0", + "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.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" - }, - "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", - "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", @@ -4939,19 +4719,17 @@ } }, "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", "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" }, @@ -4961,9 +4739,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", @@ -4976,9 +4753,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", @@ -4993,9 +4769,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", @@ -5005,20 +4780,10 @@ "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", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -5028,9 +4793,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" }, @@ -5043,9 +4807,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" }, @@ -5058,9 +4821,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" }, @@ -5068,11 +4830,15 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-report/node_modules/yallist": { + "version": "4.0.0", + "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", @@ -5084,9 +4850,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" @@ -5097,18 +4862,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" @@ -5119,15 +4882,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" }, @@ -5137,9 +4898,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" }, @@ -5149,33 +4909,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 - }, - "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 + "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" }, @@ -5184,16 +4929,14 @@ } }, "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==", - "dev": true + "version": "3.2.1", + "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" }, @@ -5201,26 +4944,78 @@ "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": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true + "version": "6.2.0", + "dev": true, + "license": "MIT" + }, + "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": { + "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.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", "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" }, @@ -5231,102 +5026,103 @@ "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", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "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", "dev": true, + "license": "MIT", "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": { "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.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", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "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 + "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 + "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": { "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": "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", "dev": true, + "license": "MIT", "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" @@ -5334,9 +5130,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" }, @@ -5345,25 +5140,22 @@ } }, "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", "dev": true, - "engines": { - "node": ">=12" + "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" }, @@ -5373,9 +5165,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" }, @@ -5390,13 +5181,13 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "license": "ISC" }, "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" }, @@ -5404,26 +5195,53 @@ "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", - "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/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", - "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" @@ -5432,20 +5250,31 @@ "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", - "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" }, @@ -5455,9 +5284,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" }, @@ -5467,9 +5295,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" }, @@ -5479,9 +5306,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" }, @@ -5491,18 +5317,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", @@ -5538,78 +5362,10 @@ "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", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5627,216 +5383,50 @@ }, "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" - }, - "engines": { - "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", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "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" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "*" } }, - "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==", + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", "dev": true, + "license": "ISC", "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" + "brace-expansion": "^2.0.1" }, "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==", + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "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==", - "dev": true + "version": "2.1.3", + "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" }, @@ -5844,26 +5434,28 @@ "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/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", - "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" }, @@ -5876,9 +5468,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" }, @@ -5887,31 +5478,19 @@ } }, "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", "dev": true, + "license": "BSD-3-Clause", "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": { "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": [ { @@ -5923,15 +5502,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", @@ -5947,9 +5526,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" }, @@ -5958,25 +5536,22 @@ } }, "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==", - "dev": true + "version": "2.0.14", + "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" }, @@ -5985,10 +5560,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", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -6001,9 +5575,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" }, @@ -6013,9 +5586,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", @@ -6052,46 +5624,73 @@ "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", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, - "node_modules/nyc/node_modules/resolve-from": { + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "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==", "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { "node": ">=8" } }, "node_modules/nyc/node_modules/y18n": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true + "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", @@ -6111,9 +5710,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" @@ -6124,39 +5722,35 @@ }, "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.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", "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.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", "dev": true, + "license": "MIT", "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" }, @@ -6167,20 +5761,41 @@ "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", - "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" }, @@ -6192,58 +5807,39 @@ } }, "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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" @@ -6251,9 +5847,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" }, @@ -6261,11 +5856,21 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ora/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/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" }, @@ -6277,25 +5882,59 @@ } }, "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==", - "dev": true + "version": "10.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", - "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" }, @@ -6308,9 +5947,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" @@ -6323,17 +5961,16 @@ } }, "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", "dev": true, + "license": "MIT", "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" @@ -6341,9 +5978,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" }, @@ -6356,9 +5992,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" @@ -6372,54 +6007,52 @@ }, "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": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "MIT", "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": { "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" }, @@ -6429,18 +6062,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", @@ -6457,9 +6088,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", @@ -6471,9 +6101,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", @@ -6486,9 +6115,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", @@ -6502,11 +6130,21 @@ "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", - "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", @@ -6529,9 +6167,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" }, @@ -6541,9 +6178,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" }, @@ -6554,11 +6190,15 @@ "node": ">=10" } }, + "node_modules/package-json/node_modules/yallist": { + "version": "4.0.0", + "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" }, @@ -6568,9 +6208,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", @@ -6586,90 +6225,84 @@ }, "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/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", - "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": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "6.2.1", "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==", - "dev": true + "license": "MIT" }, "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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" }, @@ -6679,27 +6312,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" }, @@ -6709,9 +6339,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" }, @@ -6719,20 +6348,58 @@ "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", "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=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" }, @@ -6741,16 +6408,15 @@ } }, "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", "dev": true, + "license": "MIT", "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": { @@ -6762,55 +6428,63 @@ }, "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-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.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", "dev": true, + "license": "MIT", "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-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==", + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", "dev": true, + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=12" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "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" }, @@ -6821,10 +6495,24 @@ "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", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -6839,13 +6527,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" }, @@ -6853,20 +6541,54 @@ "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", - "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/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", - "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", @@ -6879,15 +6601,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", + "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", @@ -6899,9 +6627,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" }, @@ -6911,8 +6638,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" @@ -6921,11 +6646,14 @@ "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", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -6940,9 +6668,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" }, @@ -6952,9 +6679,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" }, @@ -6966,35 +6692,44 @@ } }, "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", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/webpro" + } + ], + "license": "MIT", "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 +6738,34 @@ "release-it": "bin/release-it.js" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/release-it/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/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", "dev": true, + "license": "MIT", "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" @@ -7027,9 +6773,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" }, @@ -7039,9 +6784,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" }, @@ -7052,11 +6796,23 @@ "node": ">=10" } }, + "node_modules/release-it/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/release-it/node_modules/yargs-parser": { + "version": "21.1.1", + "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" }, @@ -7066,24 +6822,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.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -7098,24 +6851,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": "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "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" }, @@ -7128,9 +6886,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" @@ -7141,18 +6898,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" }, @@ -7164,156 +6919,56 @@ } }, "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-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==", + "version": "0.13.1", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 4" } }, - "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==", + "node_modules/reusify": { + "version": "1.0.4", "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "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==", + "node_modules/rimraf": { + "version": "3.0.2", "dev": true, + "license": "ISC", "dependencies": { - "mimic-fn": "^2.1.0" + "glob": "^7.1.3" }, - "engines": { - "node": ">=6" + "bin": { + "rimraf": "bin.js" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "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, + "node_modules/run-applescript": { + "version": "7.0.0", + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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": [ { @@ -7329,27 +6984,26 @@ "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.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", "dev": true, + "license": "MIT", "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" }, @@ -7362,9 +7016,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": [ { "type": "github", @@ -7378,42 +7029,42 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "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", "dev": true, + "license": "MIT", "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" } }, "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" }, @@ -7426,9 +7077,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" }, @@ -7438,9 +7088,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" }, @@ -7451,26 +7100,114 @@ "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", + "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", - "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/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", - "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.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.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "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", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -7480,11 +7217,17 @@ "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", - "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" }, @@ -7494,18 +7237,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", @@ -7519,10 +7260,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", "dev": true, + "license": "MIT", "dependencies": { "ansi-sequence-parser": "^1.1.0", "jsonc-parser": "^3.2.0", @@ -7531,14 +7271,19 @@ } }, "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==", + "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" @@ -7546,21 +7291,19 @@ }, "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": "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", "dev": true, + "license": "BSD-3-Clause", "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": { @@ -7570,27 +7313,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/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", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7599,12 +7331,11 @@ } }, "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7612,9 +7343,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" @@ -7622,9 +7352,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" @@ -7636,9 +7365,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", @@ -7650,34 +7378,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/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", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", @@ -7692,20 +7407,25 @@ }, "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/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.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", "dev": true, - "dependencies": { - "bl": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7713,9 +7433,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" }, @@ -7723,20 +7442,28 @@ "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", - "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", @@ -7748,9 +7475,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", @@ -7765,9 +7491,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", @@ -7779,9 +7504,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", @@ -7793,9 +7517,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" }, @@ -7805,18 +7528,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" }, @@ -7825,19 +7546,20 @@ } }, "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", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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" }, @@ -7847,30 +7569,30 @@ }, "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": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "8.1.1", "dev": true, + "license": "MIT", "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": { "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" }, @@ -7880,9 +7602,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", @@ -7892,29 +7613,10 @@ "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", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, + "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -7924,18 +7626,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" }, @@ -7943,17 +7643,20 @@ "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/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", - "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" }, @@ -7963,30 +7666,18 @@ }, "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/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" - } - }, "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==", + "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", "@tsconfig/node10": "^1.0.7", @@ -8025,65 +7716,72 @@ } } }, - "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==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "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 + "license": "0BSD" }, - "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", "dev": true, + "license": "MIT", "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": { "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.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", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, "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", @@ -8095,9 +7793,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", @@ -8113,9 +7810,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", @@ -8132,9 +7828,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", @@ -8146,23 +7841,21 @@ }, "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.1", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", - "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", + "version": "0.25.7", "dev": true, + "license": "Apache-2.0", "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,23 +7864,21 @@ "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": { "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" }, @@ -8199,10 +7890,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", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8211,11 +7901,23 @@ "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", - "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", @@ -8223,14 +7925,29 @@ "which-boxed-primitive": "^1.0.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "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", + "dev": true, + "license": "MIT", + "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", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -8242,33 +7959,30 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true + "version": "6.0.1", + "dev": true, + "license": "ISC" }, "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", "dev": true, + "license": "MIT", "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==", + "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": ">=8" + "node": ">= 0.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", "dev": true, "funding": [ { @@ -8284,6 +7998,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -8296,38 +8011,45 @@ } }, "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", "dev": true, + "license": "BSD-2-Clause", "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", + "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", - "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" }, @@ -8337,9 +8059,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" }, @@ -8350,35 +8071,37 @@ "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==", + "node_modules/update-notifier/node_modules/yallist": { + "version": "4.0.0", "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } + "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/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", - "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" } @@ -8387,59 +8110,49 @@ "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 + "dev": true, + "license": "MIT" + }, + "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", - "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.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.2", "dev": true, + "license": "MIT", "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", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -8452,9 +8165,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", @@ -8468,21 +8180,19 @@ }, "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.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.14", "dev": true, + "license": "MIT", "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" @@ -8493,9 +8203,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" }, @@ -8508,9 +8217,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" }, @@ -8520,15 +8228,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", @@ -8543,9 +8249,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" }, @@ -8558,15 +8263,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" }, @@ -8579,9 +8282,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", @@ -8600,41 +8302,37 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "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==", + "node_modules/windows-release/node_modules/get-stream": { + "version": "6.0.1", "dev": true, + "license": "MIT", "engines": { - "node": ">=10.17.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/windows-release/node_modules/human-signals": { + "version": "2.1.0", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "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" }, @@ -8644,9 +8342,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" }, @@ -8659,24 +8356,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", @@ -8688,15 +8382,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", @@ -8706,9 +8398,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" }, @@ -8718,50 +8409,46 @@ }, "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": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "3.1.1", + "dev": true, + "license": "ISC" }, "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", "dev": true, + "license": "MIT", "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", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" + "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", @@ -8774,9 +8461,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" }, @@ -8786,9 +8472,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" }, @@ -8801,15 +8486,15 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8817,147 +8502,226 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/authx": { + "name": "@redis/authx", + "version": "5.0.0-next.5", + "extraneous": true, + "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": "1.2.0", + "version": "5.0.1", "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": "^5.0.1" } }, "packages/client": { "name": "@redis/client", - "version": "1.6.0", + "version": "5.0.1", "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/entraid": { + "name": "@redis/entraid", + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.7.0", + "@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/client": "^5.0.1" + } + }, + "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": "1.1.1", + "version": "5.0.0-next.6", + "extraneous": true, "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": "^5.0.0-next.6" } }, "packages/json": { "name": "@redis/json", - "version": "1.0.7", + "version": "5.0.1", "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": "^5.0.1" + } + }, + "packages/redis": { + "version": "5.0.0", + "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" + }, + "engines": { + "node": ">= 18" } }, "packages/search": { "name": "@redis/search", - "version": "1.2.0", + "version": "5.0.1", "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": "^5.0.1" } }, "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", + "dev": true, + "license": "ISC", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "packages/time-series": { "name": "@redis/time-series", - "version": "1.1.0", + "version": "5.0.1", "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": "^5.0.1" } } } diff --git a/package.json b/package.json index e8ceef7173d..2ff2d4825ae 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,27 @@ { - "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-single": "TS_NODE_PROJECT='./packages/test-utils/tsconfig.json' mocha --require ts-node/register/transpile-only ", "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 --out ./documentation", "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", + "ts-node": "^10.9.2" + } } 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..a229936c7df 100644 --- a/packages/bloom/lib/commands/bloom/ADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/ADD.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['BF.ADD', 'key', 'item'] - ); - }); +describe('BF.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ADD, '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..e12d9cfa1d2 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -1,7 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 function transformArguments(key: string, item: string): Array { - return ['BF.ADD', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 4d5620ea196..32a28cdf6f7 100644 --- a/packages/bloom/lib/commands/bloom/CARD.spec.ts +++ b/packages/bloom/lib/commands/bloom/CARD.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF CARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('bloom'), - ['BF.CARD', 'bloom'] - ); - }); +describe('BF.CARD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CARD, '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..c2f9aeb00fc 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -1,9 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string): Array { - return ['BF.CARD', key]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 1088e739e61..4d2cc70074a 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF EXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['BF.EXISTS', 'key', 'item'] - ); - }); +describe('BF.EXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(EXISTS, '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..b3f19af9516 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -1,9 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 const IS_READ_ONLY = true; - -export function transformArguments(key: string, item: string): Array { - return ['BF.EXISTS', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: true, + 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 7a5e5724c22..0dbe5cb1f43 100644 --- a/packages/bloom/lib/commands/bloom/INFO.spec.ts +++ b/packages/bloom/lib/commands/bloom/INFO.spec.ts @@ -1,24 +1,27 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('bloom'), - ['BF.INFO', 'bloom'] - ); - }); +describe('BF.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, '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..b715bf57738 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -1,38 +1,25 @@ -export const FIRST_KEY_INDEX = 1; +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 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 function transformReply(reply: InfoRawReply): InfoReply { - return { - capacity: reply[1], - size: reply[3], - numberOfFilters: reply[5], - numberOfInsertedItems: reply[7], - expansionRate: reply[9] - }; -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('BF.INFO'); + parser.pushKey(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..a9b544a51ae 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.spec.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.spec.ts @@ -1,69 +1,70 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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( + parseArgs(INSERT, '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..b8dcef325f8 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -1,45 +1,49 @@ -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 { 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; + ERROR?: number; + EXPANSION?: number; + NOCREATE?: boolean; + NONSCALING?: boolean; } -export function transformArguments( - key: string, - items: RedisCommandArgument | Array, - options?: InsertOptions -): RedisCommandArguments { - const args = ['BF.INSERT', key]; - - if (options?.CAPACITY) { - args.push('CAPACITY', options.CAPACITY.toString()); +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + items: RedisVariadicArgument, + options?: BfInsertOptions + ) { + parser.push('BF.INSERT'); + parser.pushKey(key); + + if (options?.CAPACITY !== undefined) { + parser.push('CAPACITY', options.CAPACITY.toString()); } - if (options?.ERROR) { - args.push('ERROR', options.ERROR.toString()); + if (options?.ERROR !== undefined) { + parser.push('ERROR', options.ERROR.toString()); } - if (options?.EXPANSION) { - args.push('EXPANSION', options.EXPANSION.toString()); + if (options?.EXPANSION !== undefined) { + 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 pushVerdictArguments(args, items); -} - -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + 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 19634cb4a78..40e24f96c39 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts @@ -1,28 +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF LOADCHUNK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, ''), - ['BF.LOADCHUNK', 'key', '0', ''] - ); - }); +describe('BF.LOADCHUNK', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LOADCHUNK, '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..ef3cc4a3e12 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: false, + 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 784f99926ff..5eb39ee73d4 100644 --- a/packages/bloom/lib/commands/bloom/MADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/MADD.spec.ts @@ -1,19 +1,20 @@ -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 { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF MADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['BF.MADD', 'key', '1', '2'] - ); - }); +describe('BF.MADD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MADD, '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..efbd932b403 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -1,7 +1,14 @@ -export const FIRST_KEY_INDEX = 1; +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 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 { + IS_READ_ONLY: false, + 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 027e51d2c43..60c09b00f17 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF MEXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['BF.MEXISTS', 'key', '1', '2'] - ); - }); +describe('BF.MEXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MEXISTS, '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..a5a311a8e4c 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -1,9 +1,14 @@ -export const FIRST_KEY_INDEX = 1; +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 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 { + IS_READ_ONLY: true, + 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 bc872f9c3f7..803577b350b 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts @@ -1,49 +1,50 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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..00f17c1889f 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -1,27 +1,31 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, errorRate: number, capacity: number, - options?: ReserveOptions -): Array { - const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()]; + options?: BfReserveOptions + ) { + 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; -} - -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..a41a6e8e466 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts @@ -1,22 +1,23 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('BF SCANDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['BF.SCANDUMP', 'key', '0'] - ); - }); +describe('BF.SCANDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SCANDUMP, '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..d0472b649c5 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -1,24 +1,17 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - 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 { +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { + parser.push('BF.SCANDUMP'); + parser.pushKey(key); + parser.push(iterator.toString()); + }, + 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..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,41 +1,43 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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..a011957ece6 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['CMS.INCRBY', key]; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + items: BfIncrByItem | Array + ) { + parser.push('CMS.INCRBY'); + parser.pushKey(key); if (Array.isArray(items)) { - for (const item of items) { - pushIncrByItem(args, item); - } + for (const item of items) { + pushIncrByItem(parser, item); + } } else { - pushIncrByItem(args, items); + pushIncrByItem(parser, items); } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; - return args; +function pushIncrByItem(parser: CommandParser, { item, incrementBy }: BfIncrByItem): void { + parser.push(item, incrementBy.toString()); } - -function pushIncrByItem(args: Array, { item, incrementBy }: IncrByItem): 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..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,25 +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CMS INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['CMS.INFO', 'key'] - ); - }); +describe('CMS.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, '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..fef1cac97e7 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -1,30 +1,29 @@ -export const FIRST_KEY_INDEX = 1; +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 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 { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('CMS.INFO'); + parser.pushKey(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..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,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CMS INITBYDIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1000, 5), - ['CMS.INITBYDIM', 'key', '1000', '5'] - ); - }); +describe('CMS.INITBYDIM', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INITBYDIM, '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..44e6a75952f 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, width: number, depth: number): Array { - return ['CMS.INITBYDIM', key, width.toString(), depth.toString()]; -} - -export declare function transformReply(): 'OK'; +export default { + IS_READ_ONLY: false, + 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 004d3df14ef..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,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INITBYPROB, '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..3b96120bd04 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, error: number, probability: number): Array { - return ['CMS.INITBYPROB', key, error.toString(), probability.toString()]; -} - -export declare function transformReply(): 'OK'; +export default { + IS_READ_ONLY: false, + 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 cf234e5734f..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,36 +1,35 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MERGE, '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( + parseArgs(MERGE, '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..4d959bd619d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -1,37 +1,39 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 function transformArguments(dest: string, src: Sketches): Array { - const args = [ - 'CMS.MERGE', - dest, - src.length.toString() - ]; - - if (isStringSketches(src)) { - args.push(...src); +export type BfMergeSketches = Array | Array; + +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + destination: RedisArgument, + source: BfMergeSketches + ) { + parser.push('CMS.MERGE'); + parser.pushKey(destination); + parser.push(source.length.toString()); + + if (isPlainSketches(source)) { + parser.pushVariadic(source); } else { - for (const sketch of src) { - args.push(sketch.name); - } - - args.push('WEIGHTS'); - for (const sketch of src) { - args.push(sketch.weight.toString()); - } + for (let i = 0; i < source.length; i++) { + parser.push(source[i].name); + } + parser.push('WEIGHTS'); + for (let i = 0; i < source.length; i++) { + parser.push(source[i].weight.toString()) + } } + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; - return args; +function isPlainSketches(src: BfMergeSketches): src is Array { + return typeof src[0] === 'string' || src[0] instanceof Buffer; } - -function isStringSketches(src: Sketches): src is Array { - return typeof src[0] === 'string'; -} - -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..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,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 './QUERY'; +import QUERY from './QUERY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CMS QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CMS.QUERY', 'key', 'item'] - ); - }); +describe('CMS.QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(QUERY, '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..b55b51d1bbd 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -1,15 +1,13 @@ -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 { 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, + 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 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..7fa518fea84 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts @@ -1,19 +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 './ADD'; +import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.ADD', 'key', 'item'] - ); - }); +describe('CF.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ADD, '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..37a5d1b5b86 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -1,7 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 function transformArguments(key: string, item: string): Array { - return ['CF.ADD', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 ddd9f922b13..c142733ce40 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts @@ -1,21 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF ADDNX', () => { - describe('transformArguments', () => { - it('basic add', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.ADDNX', 'key', 'item'] - ); - }); - }); +describe('CF.ADDNX', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ADDNX, '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..ceaf62be21c 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -1,7 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 function transformArguments(key: string, item: string): Array { - return ['CF.ADDNX', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 29f5b415935..9393494d852 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.COUNT', 'key', 'item'] - ); - }); +describe('CF.COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COUNT, '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..f0cd5a72105 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, item: string): Array { - return ['CF.COUNT', key, item]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 03da65881c1..41ed653bfc9 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.DEL', 'key', 'item'] - ); - }); +describe('CF.DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DEL, '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..c97b7c2d9fc 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -1,7 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 function transformArguments(key: string, item: string): Array { - return ['CF.DEL', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 e281bde6d8a..f77a9d69eff 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF EXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.EXISTS', 'key', 'item'] - ); - }); +describe('CF.EXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(EXISTS, '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..2299cb3de99 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -1,9 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +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 const IS_READ_ONLY = true; - -export function transformArguments(key: string, item: string): Array { - return ['CF.EXISTS', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 c2ac5de6fe0..c5503ed113b 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.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 './INFO'; +import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('cuckoo'), - ['CF.INFO', 'cuckoo'] - ); - }); +describe('CF.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, '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..6a8f06f1e77 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -1,50 +1,28 @@ -export const FIRST_KEY_INDEX = 1; +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 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 { - 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] - }; -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('CF.INFO'); + parser.pushKey(key); + }, + 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..dc2bd574517 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts @@ -1,22 +1,23 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INSERT, '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..3ad3feee16d 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -1,18 +1,37 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { InsertOptions, pushInsertOptions } from "."; +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 const FIRST_KEY_INDEX = 1; +export interface CfInsertOptions { + CAPACITY?: number; + NOCREATE?: boolean; +} + +export function parseCfInsertArguments( + parser: CommandParser, + key: RedisArgument, + items: RedisVariadicArgument, + options?: CfInsertOptions +) { + parser.pushKey(key); + + if (options?.CAPACITY !== undefined) { + parser.push('CAPACITY', options.CAPACITY.toString()); + } + + if (options?.NOCREATE) { + parser.push('NOCREATE'); + } -export function transformArguments( - key: string, - items: string | Array, - options?: InsertOptions -): RedisCommandArguments { - return pushInsertOptions( - ['CF.INSERT', key], - items, - options - ); + parser.push('ITEMS'); + parser.pushVariadic(items); } -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 7b1d974e5a6..648d9be7ac8 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts @@ -1,22 +1,23 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INSERTNX, '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..7ddc952e2fa 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,18 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { InsertOptions, pushInsertOptions } from "."; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import INSERT, { parseCfInsertArguments } 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 { + IS_READ_ONLY: INSERT.IS_READ_ONLY, + 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 ca3d6f2f8f7..5415c787dda 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.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 './LOADCHUNK'; +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( - transformArguments('item', 0, ''), - ['CF.LOADCHUNK', 'item', '0', ''] - ); - }); +describe('CF.LOADCHUNK', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LOADCHUNK, '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..8fb21be8e0d 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - return ['CF.LOADCHUNK', key, iterator.toString(), chunk]; -} - -export declare function transformReply(): 'OK'; +export default { + IS_READ_ONLY: false, + 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 3145a222c5e..53546e4156e 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts @@ -1,48 +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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..2685b0db06d 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -1,31 +1,35 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, capacity: number, - options?: ReserveOptions -): Array { - const args = ['CF.RESERVE', key, capacity.toString()]; + options?: CfReserveOptions + ) { + parser.push('CF.RESERVE'); + parser.pushKey(key); + parser.push(capacity.toString()); - if (options?.BUCKETSIZE) { - args.push('BUCKETSIZE', options.BUCKETSIZE.toString()); + if (options?.BUCKETSIZE !== undefined) { + parser.push('BUCKETSIZE', options.BUCKETSIZE.toString()); } - if (options?.MAXITERATIONS) { - args.push('MAXITERATIONS', options.MAXITERATIONS.toString()); + if (options?.MAXITERATIONS !== undefined) { + parser.push('MAXITERATIONS', options.MAXITERATIONS.toString()); } - if (options?.EXPANSION) { - args.push('EXPANSION', options.EXPANSION.toString()); + if (options?.EXPANSION !== undefined) { + parser.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..60a57ac46ab 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts @@ -1,23 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CF SCANDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['CF.SCANDUMP', 'key', '0'] - ); - }); +describe('CF.SCANDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SCANDUMP, '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..25ef2c3f6da 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -1,22 +1,17 @@ -export const FIRST_KEY_INDEX = 1; +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 function transformArguments(key: string, iterator: number): Array { - 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 { +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { + parser.push('CF.SCANDUMP'); + parser.pushKey(key); + parser.push(iterator.toString()); + }, + 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..7578fb9378b 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.spec.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.ADD', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ADD, '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..5534d58065b 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -1,17 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { + parser.push('TDIGEST.ADD'); + parser.pushKey(key); -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { - const args = ['TDIGEST.ADD', key]; - for (const item of values) { - args.push(item.toString()); + for (const value of values) { + parser.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..81a2c75dff5 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.BYRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.BYRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(BYRANK, '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..9c1ab0059f3 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -1,19 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 const FIRST_KEY_INDEX = 1; +export function transformByRankArguments( + parser: CommandParser, + key: RedisArgument, + ranks: Array +) { + parser.pushKey(key); -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - ranks: Array -): RedisCommandArguments { - const args = ['TDIGEST.BYRANK', key]; - for (const rank of ranks) { - args.push(rank.toString()); - } - - return args; + for (const rank of ranks) { + parser.push(rank.toString()); + } } -export { transformDoublesReply as transformReply } from '.'; +export default { + IS_READ_ONLY: true, + 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 c094f36e71d..c8f794bef57 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.BYREVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.BYREVRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(BYREVRANK, '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..8721c081e7a 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -1,19 +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, - 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 { + IS_READ_ONLY: BYRANK.IS_READ_ONLY, + 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 36d3564f62c..2689bf2fc9a 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.CDF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.CDF', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CDF, '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..4d1d8ea2786 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -1,19 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { + parser.push('TDIGEST.CDF'); + parser.pushKey(key); -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { - const args = ['TDIGEST.CDF', key]; for (const item of values) { - args.push(item.toString()); + parser.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..0f218e07ab8 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.CREATE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.CREATE', 'key'] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(CREATE, 'key'), + ['TDIGEST.CREATE', 'key'] + ); + }); - it('with COMPRESSION', () => { - assert.deepEqual( - transformArguments('key', { - COMPRESSION: 100 - }), - ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100'] - ); - }); + it('with COMPRESSION', () => { + assert.deepEqual( + parseArgs(CREATE, '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..58b1e008284 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -1,16 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { CompressionOption, pushCompressionArgument } from '.'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: TDigestCreateOptions) { + parser.push('TDIGEST.CREATE'); + parser.pushKey(key); + + if (options?.COMPRESSION !== undefined) { + parser.push('COMPRESSION', options.COMPRESSION.toString()); + } + }, + 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..d5b8b3e13ed 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.spec.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.spec.ts @@ -1,25 +1,31 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.INFO', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, '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..2cb9e93443c 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -1,51 +1,29 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 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 { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TDIGEST.INFO'); + parser.pushKey(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..920c9d11391 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.MAX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.MAX', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MAX, '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..140db6a3e48 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -1,14 +1,12 @@ -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 { 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, + 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 1205cdd9216..f2a7c1a1192 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts @@ -1,50 +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 './MERGE'; +import MERGE from './MERGE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; 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( + parseArgs(MERGE, '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( + parseArgs(MERGE, '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( + parseArgs(MERGE, '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( + parseArgs(MERGE, '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..80049d1e540 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -1,30 +1,31 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { CompressionOption, pushCompressionArgument } from '.'; +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 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + destination: RedisArgument, + source: RedisVariadicArgument, + options?: TDigestMergeOptions + ) { + parser.push('TDIGEST.MERGE'); + parser.pushKey(destination); + parser.pushKeysLength(source); - pushCompressionArgument(args, options); + if (options?.COMPRESSION !== undefined) { + parser.push('COMPRESSION', options.COMPRESSION.toString()); + } if (options?.OVERRIDE) { - args.push('OVERRIDE'); + parser.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..278248ea465 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.MIN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.MIN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MIN, '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..d6e56fb672e 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -1,14 +1,12 @@ -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 { 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, + 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 7790debf0de..ac7249d12d9 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts @@ -1,24 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.QUANTILE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.QUANTILE', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(QUANTILE, '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..1c27b5f6ec6 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -1,23 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 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 { + IS_READ_ONLY: true, + 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; -} - -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..f1747662f0a 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.RANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.RANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RANK, '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..053c0c544e9 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -1,19 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +export function transformRankArguments( + parser: CommandParser, + key: RedisArgument, + values: Array +) { + parser.pushKey(key); -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { - const args = ['TDIGEST.RANK', key]; - for (const item of values) { - args.push(item.toString()); - } - - return args; + for (const value of values) { + parser.push(value.toString()); + } } -export declare function transformReply(): Array; +export default { + IS_READ_ONLY: true, + 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 036fbebc8cc..8e1fc12e6e3 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.RESET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.RESET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RESET, '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..c2bda72d6d4 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['TDIGEST.RESET', key]; -} - -export declare function transformReply(): 'OK'; +export default { + IS_READ_ONLY: false, + 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 21d16661dfe..be7b23b2238 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.REVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.REVRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(REVRANK, '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..e7f2357a2f8 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -1,19 +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, - 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 { + IS_READ_ONLY: RANK.IS_READ_ONLY, + 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 dd07f325c8d..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,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(TRIMMED_MEAN, '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..1fd6360ab65 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 { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, lowCutPercentile: number, highCutPercentile: number -): RedisCommandArguments { - return [ - 'TDIGEST.TRIMMED_MEAN', - key, - lowCutPercentile.toString(), - highCutPercentile.toString() - ]; -} - -export { transformDoubleReply as transformReply } from '.'; + ) { + 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.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..15a7a9ce1dd 100644 --- a/packages/bloom/lib/commands/top-k/ADD.spec.ts +++ b/packages/bloom/lib/commands/top-k/ADD.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 './ADD'; +import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TOPK ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.ADD', 'key', 'item'] - ); - }); +describe('TOPK.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ADD, '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..244e6209c91 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -1,13 +1,13 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/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 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 { + IS_READ_ONLY: false, + 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 318fc74c679..a242edfef8a 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.spec.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TOPK COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.COUNT', 'key', 'item'] - ); - }); +describe('TOPK.COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COUNT, '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..7e75a3b68aa 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -1,15 +1,13 @@ -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 { 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, + 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 b23ca6e0ed1..94e5b1d7058 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts @@ -1,42 +1,43 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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..9e9a49e18f9 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['TOPK.INCRBY', key]; +function pushIncrByItem(parser: CommandParser, { item, incrementBy }: TopKIncrByItem) { + parser.push(item, incrementBy.toString()); +} + +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + items: TopKIncrByItem | Array + ) { + parser.push('TOPK.INCRBY'); + parser.pushKey(key); if (Array.isArray(items)) { - for (const item of items) { - pushIncrByItem(args, item); - } + for (const item of items) { + pushIncrByItem(parser, item); + } } else { - pushIncrByItem(args, items); + pushIncrByItem(parser, 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..2efbf0bdbef 100644 --- a/packages/bloom/lib/commands/top-k/INFO.spec.ts +++ b/packages/bloom/lib/commands/top-k/INFO.spec.ts @@ -1,23 +1,27 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TOPK.INFO', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, '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..53da265c1f2 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -1,34 +1,27 @@ -export const FIRST_KEY_INDEX = 1; +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 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 { - return ['TOPK.INFO', key]; -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TOPK.INFO'); + parser.pushKey(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..8f5d0efa4db 100644 --- a/packages/bloom/lib/commands/top-k/LIST.spec.ts +++ b/packages/bloom/lib/commands/top-k/LIST.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TOPK LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TOPK.LIST', 'key'] - ); - }); +describe('TOPK.LIST', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LIST, '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..d7adeaa193c 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -1,9 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string): Array { - return ['TOPK.LIST', key]; -} - -export declare function transformReply(): Array; +export default { + IS_READ_ONLY: true, + 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 1e55239c243..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,30 +1,28 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(LIST_WITHCOUNT, '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..2c0f10e785b 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -1,26 +1,26 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - return ['TOPK.LIST', key, 'WITHCOUNT']; -} - -type ListWithCountRawReply = Array; - -type ListWithCountReply = Array<{ - item: string, - count: number -}>; - -export function transformReply(rawReply: ListWithCountRawReply): ListWithCountReply { - const reply: ListWithCountReply = []; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TOPK.LIST'); + parser.pushKey(key); + parser.push('WITHCOUNT'); + }, + 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..3651ec5d37b 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.spec.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TOPK QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.QUERY', 'key', 'item'] - ); - }); +describe('TOPK.QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(QUERY, '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..a6fb4bae69e 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -1,15 +1,13 @@ -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 { 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, + 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 54600c0e4f5..aa8d194f940 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts @@ -1,32 +1,33 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(RESERVE, '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( + parseArgs(RESERVE, '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..ee3ee9a8cf4 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['TOPK.RESERVE', key, topK.toString()]; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, topK: number, options?: TopKReserveOptions) { + parser.push('TOPK.RESERVE'); + parser.pushKey(key); + parser.push(topK.toString()); if (options) { - args.push( - options.width.toString(), - options.depth.toString(), - options.decay.toString() - ); + parser.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..71b423b41ea 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -1,19 +1,19 @@ import TestUtils from '@redis/test-utils'; import RedisBloomModules from '.'; -export default new TestUtils({ - dockerImageName: 'redislabs/rebloom', - dockerImageVersionArgument: 'redisbloom-version', - defaultDockerVersion: 'edge' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' }); 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..0772fcd3bb8 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,30 +1,24 @@ { "name": "@redis/bloom", - "version": "1.2.0", + "version": "5.0.1", "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": "^5.0.1" }, "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..e426badf126 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 { GEO_REPLY_WITH, GeoReplyWith } from './lib/commands/GEOSEARCH_WITH'; export { SetOptions } from './lib/commands/SET'; -export { RedisFlushModes } from './lib/commands/FLUSHALL'; +export { REDIS_FLUSH_MODES } 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..995650627f1 --- /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: ReadonlyArray): ReadonlyArray { + 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..692c433a49d --- /dev/null +++ b/packages/client/lib/RESP/types.ts @@ -0,0 +1,401 @@ +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'; +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 = { + CACHEABLE?: boolean; + IS_READ_ONLY?: boolean; + /** + * @internal + * TODO: remove once `POLICIES` is implemented + */ + IS_FORWARD_COMMAND?: boolean; + NOT_KEYED_COMMAND?: true; + // POLICIES?: CommandPolicies; + parseCommand(this: void, parser: CommandParser, ...args: Array): void; + 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: Tail>) => 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/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/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..15e8a747b98 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 { TypeMapping, ReplyUnion, RespVersions, RedisArgument } 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: ReadonlyArray; + 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: ReadonlyArray, + 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: ReadonlyArray + 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..c71cf1a1fad 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1,1076 +1,868 @@ -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 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'; -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'; +import { CommandParser } from './parser'; 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, + 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) 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 - ); - }); + describe('parseURL', () => { + 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' + }) + } + }; - it('redis://localhost', () => { - assert.deepEqual( - RedisClient.parseURL('redis://localhost'), - { - socket: { - host: 'localhost', - } - } - ); - }); - }); + // Compare everything except the credentials function + const { credentialsProvider: resultCredProvider, ...resultRest } = result; + const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected; - 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 - }); + // Compare non-function properties + assert.deepEqual(resultRest, expectedRest); - testUtils.testWithClient('should set default lib name and version', async client => { - const clientInfo = await client.clientInfo(); + if(result.credentialsProvider.type === 'async-credentials-provider' + && expected.credentialsProvider.type === 'async-credentials-provider') { - assert.equal(clientInfo.libName, 'node-redis'); - assert.equal(clientInfo.libVer, version); - }, { - ...GLOBAL.SERVERS.PASSWORD, - minimumDockerVersion: [7, 2] - }); + // 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'); + } - 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] - }); - 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] - }); }); - 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] - }); - }); - - testUtils.testWithClient('should set connection name', async client => { - assert.equal( - await client.clientGetName(), - 'name' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - name: 'name' + 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' } - }); + }; - 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 - } - }); + // Compare everything except the credentials function + const { credentialsProvider: resultCredProvider, ...resultRest } = result; + const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected; - 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 - } - }); + // Compare non-function properties + assert.deepEqual(resultRest, expectedRest); + assert.equal(resultCredProvider.type, expectedCredProvider.type); - 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 - } - }); + if (result.credentialsProvider.type === 'async-credentials-provider' && + expected.credentialsProvider.type === 'async-credentials-provider') { - testUtils.testWithClient('client.hGetAll should reply with error', async client => { - await assert.rejects( - promisify(client.hGetAll).call(client) - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); + // Compare the actual output of the credentials functions + const resultCreds = await result.credentialsProvider.credentials(); + const expectedCreds = await expected.credentialsProvider.credentials(); + assert.deepEqual(resultCreds, expectedCreds); - 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 - } - }); + } else { + assert.fail('Credentials provider type mismatch'); + } - 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 - } - }); + it('Invalid protocol', () => { + assert.throws( + () => RedisClient.parseURL('redi://user:secret@localhost:6379/0'), + TypeError + ); + }); - 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 - } - }); + it('Invalid pathname', () => { + assert.throws( + () => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'), + TypeError + ); + }); - 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 - } - }); + 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('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, + 2 + ); + }, { + ...GLOBAL.SERVERS.PASSWORD, + clientOptions: { + ...GLOBAL.SERVERS.PASSWORD.clientOptions, + database: 2 + }, + minimumDockerVersion: [6, 2] + }); + }); - 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('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('success', async client => { + await client.sendCommand(['PING'], { + abortSignal: new AbortController().signal }); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => { - assert.deepEqual( - await client.multi() - .ping() - .v4.ping() - .v4.exec(), - ['PONG', 'PONG'] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); + testUtils.testWithClient('AbortError', client => { + const controller = new AbortController(); + controller.abort(); - 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 - } - } - }); + return assert.rejects( + client.sendCommand(['PING'], { + abortSignal: controller.signal + }), + AbortError + ); + }, GLOBAL.SERVERS.OPEN); + }); - 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('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.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); + // @ts-ignore TS2802 + 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: { + parseCommand(parser: CommandParser, message: string) { + parser.push('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..f48e03d0c19 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,883 +1,1239 @@ -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 { 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'; +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, TransformReply } 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, parseArgs, pushVariadicArguments } from '../commands/generic-transformers'; +import { BasicCommandParser, CommandParser } from './parser'; 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; + + /** + * 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)) + */ + 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 - }); + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); - if (Client !== RedisClient) { - Client.prototype.Multi = RedisClientMultiCommand.extend(extensions); - } - - return Client; - } + return async function (this: ProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - static create< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(options?: RedisClientOptions): RedisClientType { - return new (RedisClient.extend(options))(options); + return this._self._executeCommand(command, parser, this._commandOptions, transformReply); } + } - 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 - } - }; + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); - if (protocol === 'rediss:') { - (parsed.socket as RedisTlsSocketOptions).tls = true; - } else if (protocol !== 'redis:') { - throw new TypeError('Invalid protocol'); - } - - if (port) { - (parsed.socket as TcpSocketConnectOpts).port = Number(port); - } + return async function (this: NamespaceProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - if (username) { - parsed.username = decodeURIComponent(username); - } + return this._self._executeCommand(command, parser, this._self._commandOptions, transformReply); + }; + } - if (password) { - parsed.password = decodeURIComponent(password); - } + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); - if (pathname.length > 1) { - const database = Number(pathname.substring(1)); - if (isNaN(database)) { - throw new TypeError('Invalid pathname'); - } + return async function (this: NamespaceProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); - parsed.database = database; - } + return this._self._executeCommand(fn, parser, this._self._commandOptions, transformReply); + }; + } - return parsed; - } + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); - readonly #options?: RedisClientOptions; - readonly #socket: RedisSocket; - readonly #queue: RedisCommandsQueue; - #isolationPool?: Pool>; - readonly #v4: Record = {}; - #selectedDB = 0; + return async function (this: ProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args) - get options(): RedisClientOptions | undefined { - return this.#options; + return this._executeScript(script, parser, this._commandOptions, transformReply); } + } + + 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 + } + }; - get isOpen(): boolean { - return this.#socket.isOpen; + if (protocol === 'rediss:') { + parsed!.socket!.tls = true; + } else if (protocol !== 'redis:') { + throw new TypeError('Invalid protocol'); } - get isReady(): boolean { - return this.#socket.isReady; + if (port) { + (parsed.socket as TcpSocketConnectOpts).port = Number(port); } - get isPubSubActive() { - return this.#queue.isPubSubActive; + if (username) { + parsed.username = decodeURIComponent(username); } - get v4(): Record { - if (!this.#options?.legacyMode) { - throw new Error('the client is not in "legacy mode"'); - } - - return this.#v4; + if (password) { + parsed.password = decodeURIComponent(password); } - 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(); + if (username || password) { + parsed.credentialsProvider = { + type: 'async-credentials-provider', + credentials: async () => ( + { + username: username ? decodeURIComponent(username) : undefined, + password: password ? decodeURIComponent(password) : undefined + }) + }; } - #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 (pathname.length > 1) { + const database = Number(pathname.substring(1)); + if (isNaN(database)) { + throw new TypeError('Invalid pathname'); + } - if (options?.database) { - this.#selectedDB = options.database; - } - - return options; + parsed.database = database; } - #initiateQueue(): RedisCommandsQueue { - return new RedisCommandsQueue( - this.#options?.commandsQueueMaxLength, - (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners) - ); + return parsed; + } + + readonly #options?: RedisClientOptions; + readonly #socket: RedisSocket; + readonly #queue: RedisCommandsQueue; + #selectedDB = 0; + #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; + + #credentialsSubscription: Disposable | null = null; + + 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; + } + + /** + * 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; + } + + 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 { + + // 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 + }) + }; } - #initiateSocket(): RedisSocket { - const socketInitiator = async (): Promise => { - const promises = []; + if (options?.url) { + const parsed = RedisClient.parseURL(options.url); + if (options.socket) { + parsed.socket = Object.assign(options.socket, parsed.socket); + } - if (this.#selectedDB !== 0) { - promises.push( - this.#queue.addCommand( - ['SELECT', this.#selectedDB.toString()], - { asap: true } - ) - ); - } - - if (this.#options?.readonly) { - promises.push( - this.#queue.addCommand( - COMMANDS.READONLY.transformArguments(), - { asap: true } - ) - ); - } - - if (!this.#options?.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 } - ) - ); - } - - 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; - - const promise = this.#sendCommand(transformLegacyCommandArguments(args)); - if (callback) return { - promise, - callback - }; - 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 - }); + Object.assign(options, parsed); } - async connect() { - // see comment in constructor - this.#isolationPool ??= this.#initiateIsolationPool(); - await this.#socket.connect(); - return this as unknown as RedisClientType; + if (options?.database) { + this._self.#selectedDB = options.database; } - 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 (options?.commandOptions) { + this._commandOptions = options.commandOptions; } - sendCommand( - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#sendCommand(args, options); - } - - // 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; - } - - 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 - ); - } - - executeFunction( - name: string, - fn: RedisFunction, - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#sendCommand( - fCallArguments(name, fn, args), - options - ); + return options; + } + + #initiateQueue(): RedisCommandsQueue { + return new RedisCommandsQueue( + this.#options?.RESP ?? 2, + this.#options?.commandsQueueMaxLength, + (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners) + ); + } + + /** + * @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)); + }); - 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()); + }, + 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 (cp && cp.type === 'async-credentials-provider') { + const credentials = await cp.credentials(); + if (credentials.password) { + hello.AUTH = { + username: credentials.username ?? 'default', + password: credentials.password + }; } + } - redisArgs.push(...args); + if (cp && cp.type === 'streaming-credentials-provider') { - try { - return await this.#sendCommand(redisArgs, options); - } catch (err: any) { - if (!err?.message?.startsWith?.('NOSCRIPT')) { - throw err; - } + const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + this.#credentialsSubscription = disposable; - redisArgs[0] = 'EVAL'; - redisArgs[1] = script.SCRIPT; - return this.#sendCommand(redisArgs, options); + if (credentials.password) { + hello.AUTH = { + username: credentials.username ?? 'default', + password: credentials.password + }; } - } + } - 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; - } + if (this.#options.name) { + hello.SETNAME = this.#options.name; + } - await this.#sendCommand(['SELECT', db.toString()], options); - this.#selectedDB = db; - } + commands.push( + parseArgs(HELLO, this.#options.RESP, hello) + ); + } else { - select = this.SELECT; + if (cp && cp.type === 'async-credentials-provider') { - #pubSubCommand(promise: Promise | undefined) { - if (promise === undefined) return Promise.resolve(); + const credentials = await cp.credentials(); - this.#tick(); - return promise; - } + if (credentials.username || credentials.password) { + commands.push( + parseArgs(COMMANDS.AUTH, { + username: credentials.username, + password: credentials.password ?? '' + }) + ); + } + } - SUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.CHANNELS, - channels, - listener, - bufferMode - ) - ); - } + if (cp && cp.type === 'streaming-credentials-provider') { - subscribe = this.SUBSCRIBE; + 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 ?? '' + }) + ); + } + } - UNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.CHANNELS, - channels, - listener, - bufferMode - ) + if (this.#options?.name) { + commands.push( + parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name) ); + } } - unsubscribe = this.UNSUBSCRIBE; - - PSUBSCRIBE( - patterns: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.PATTERNS, - patterns, - listener, - bufferMode - ) - ); + if (selectedDB !== 0) { + commands.push(['SELECT', this.#selectedDB.toString()]); } - pSubscribe = this.PSUBSCRIBE; - - PUNSUBSCRIBE( - patterns?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.PATTERNS, - patterns, - listener, - bufferMode - ) - ); + if (this.#options?.readonly) { + commands.push( + parseArgs(COMMANDS.READONLY) + ); } - pUnsubscribe = this.PUNSUBSCRIBE; - - SSUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.SHARDED, - channels, - listener, - bufferMode - ) + return commands; + } + + #initiateSocket(): RedisSocket { + const socketInitiator = async () => { + 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 + } + ) ); - } - - sSubscribe = this.SSUBSCRIBE; - - SUNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.SHARDED, - channels, - listener, - bufferMode - ) + } + + 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 + }) ); - } + } - sUnsubscribe = this.SUNSUBSCRIBE; + if (promises.length) { + this.#write(); + return Promise.all(promises); + } + }; - getPubSubListeners(type: PubSubType) { - return this.#queue.getPubSubListeners(type); + return new RedisSocket(socketInitiator, this.#options?.socket) + .on('data', chunk => { + try { + this.#queue.decoder.write(chunk); + } catch (err) { + this.#queue.resetDecoder(); + this.emit('error', err); + } + }) + .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.#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; + } + + /** + * @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); } - extendPubSubChannelListeners( - type: PubSubType, - channel: string, - listeners: ChannelListeners - ) { - return this.#pubSubCommand( - this.#queue.extendPubSubChannelListeners(type, channel, listeners) - ); + return reply; + } + + /** + * @internal + */ + async _executeScript( + script: RedisScript, + parser: CommandParser, + options: CommandOptions | undefined, + transformReply: TransformReply | undefined, + ) { + const args = parser.redisArgs as Array; + + let reply: ReplyUnion; + try { + reply = await this.sendCommand(args, options); + } catch (err) { + if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err; + + args[0] = 'EVAL'; + args[1] = script.SCRIPT; + reply = await this.sendCommand(args, options); } - extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) { - return this.#pubSubCommand( - this.#queue.extendPubSubListeners(type, listeners) - ); + 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()); } - 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; - }); + const promise = this._self.#queue.addCommand(args, options); + this._self.#scheduleWrite(); + return promise; + } + + 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()); } - 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); - } + 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; } - executeIsolated(fn: (client: RedisClientType) => T | Promise): Promise { - if (!this.#isolationPool) return Promise.reject(new ClientClosedError()); - return this.#isolationPool.use(fn); + 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(); } - MULTI(): RedisClientMultiCommandType { - return new (this as any).Multi( - this.multiExecutor.bind(this), - this.#options?.legacyMode - ); + if (dirtyWatch) { + throw new WatchError(dirtyWatch); } - 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(); - - const results = await promise; - - if (selectedDB !== undefined) { - this.#selectedDB = selectedDB; - } - - return results; + if (watchEpoch && watchEpoch !== this._self.#epoch) { + throw new WatchError('Client reconnected after WATCH'); } - #addMultiCommands(commands: Array, chainId?: symbol) { - return Promise.all( - commands.map(({ args }) => this.#queue.addCommand(args, { chainId })) - ); + 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 + }) + ); } - 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); - } + promises.push( + this._self.#queue.addCommand(['EXEC'], { 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(); - 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); + const results = await Promise.all(promises), + execResult = results[results.length - 1]; + + if (execResult === null) { + throw new WatchError(); } - 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 (selectedDB !== undefined) { + this._self.#selectedDB = selectedDB; } - 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); + 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; + 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 + }) + ); + } + 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 disconnect(): Promise { - if (this.#pingTimer) clearTimeout(this.#pingTimer); - this.#queue.flushAll(new DisconnectsClientError()); - this.#socket.disconnect(); - await this.#destroyIsolationPool(); + if (this._self.#monitorCallback) { + console.warn('Returning a client with active MONITOR'); + shouldReset = true; } - async #destroyIsolationPool(): Promise { - await this.#isolationPool!.drain(); - await this.#isolationPool!.clear(); - this.#isolationPool = undefined; + if (this._self.#queue.isPubSubActive) { + console.warn('Returning a client with active PubSub'); + shouldReset = true; } - 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 { + 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']); + 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); + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; + }); + } + + /** + * Destroy the client. Rejects all commands immediately. + */ + destroy() { + clearTimeout(this._self.#pingTimer); + this._self.#queue.flushAll(new DisconnectsClientError()); + this._self.#socket.destroy(); + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; + } + + 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..a687655b60a 100644 --- a/packages/client/lib/client/multi-command.ts +++ b/packages/client/lib/client/multi-command.ts @@ -1,200 +1,239 @@ -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'; +import { BasicCommandParser } from './parser'; +import { Tail } from '../commands/generic-transformers'; 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: Tail>) => 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) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + + return this.addCommand( + redisArgs, + transformReply + ); + }; + } + + 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( + redisArgs, + transformReply + ); + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); -attachCommands({ - BaseClass: RedisClientMultiCommand, - commands: COMMANDS, - executor: RedisClientMultiCommand.prototype.commandsExecutor -}); + return function (this: { _self: RedisClientMultiCommand }, ...args: Array) { + 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 + ); + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); + + return function (this: RedisClientMultiCommand, ...args: Array) { + const parser = new BasicCommandParser(); + script.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + + return this.#addScript( + script, + redisArgs, + transformReply + ); + }; + } + + 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: RedisMultiCommand + readonly #executeMulti: ExecuteMulti; + readonly #executePipeline: ExecuteMulti; + + #selectedDB?: number; + + constructor(executeMulti: ExecuteMulti, executePipeline: ExecuteMulti, typeMapping?: TypeMapping) { + this.#multi = new RedisMultiCommand(typeMapping); + this.#executeMulti = executeMulti; + this.#executePipeline = executePipeline; + } + + 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; + } + + #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) + ) 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) + ) as MultiReplyType; + } + + execAsPipelineTyped() { + return this.execAsPipeline(); + } +} 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.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..a08377e3d38 --- /dev/null +++ b/packages/client/lib/client/pool.ts @@ -0,0 +1,467 @@ +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'; +import { BasicCommandParser } from './parser'; + +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 parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + 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 parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + 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); + const transformReply = getTransformReply(fn, resp); + + return async function (this: NamespaceProxyPool, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + return this._self.execute(client => client._executeCommand(fn, parser, this._self._commandOptions, transformReply)) }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + + return async function (this: ProxyPool, ...args: Array) { + const parser = new BasicCommandParser(); + parser.pushVariadic(prefix); + script.parseCommand(parser, ...args); + + return this.execute(client => client._executeScript(script, parser, this._commandOptions, transformReply)) + }; + } + + 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)); + } + + 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..36afa36c04a 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..c2fde197f4f 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -1,621 +1,615 @@ -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) { + try { + const addressesInUse = new Set(), + promises: Array> = [], + eagerConnect = this.#options.minimizeConnections !== true; + + 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) + }; - 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 = { + ...this.#options.defaults.socket, + ...options?.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, index >= this.masters.length); + + 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..12928e71f12 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -1,424 +1,619 @@ -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'; +import ASKING from '../commands/ASKING'; +import { BasicCommandParser } from '../client/parser'; +import { parseArgs } from '../commands/generic-transformers'; + +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['NOT_KEYED_COMMAND'] extends true ? ( + 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 #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); + + return this._self.#execute( + parser.firstKey, + command.IS_READ_ONLY, + this._commandOptions, + (client, opts) => client._executeCommand(command, parser, opts, transformReply) + ); + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + + return async function (this: NamespaceProxyCluster, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + return this._self.#execute( + parser.firstKey, + command.IS_READ_ONLY, + this._self._commandOptions, + (client, opts) => client._executeCommand(command, parser, opts, transformReply) + ); + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + + return async function (this: NamespaceProxyCluster, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + return this._self.#execute( + parser.firstKey, + fn.IS_READ_ONLY, + this._self._commandOptions, + (client, opts) => client._executeCommand(fn, parser, opts, transformReply) + ); + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + + return async function (this: ProxyCluster, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args); + + return this._self.#execute( + parser.firstKey, + script.IS_READ_ONLY, + this._commandOptions, + (client, opts) => client._executeScript(script, parser, opts, transformReply) + ); + }; + } + + 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, + options: ClusterCommandOptions | undefined, + fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise + ): Promise { + const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; + let client = await this.#slots.getClient(firstKey, isReadonly); + let i = 0; + let myOpts = options; + + while (true) { + try { + 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; } - } - 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; + 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); + } - async SUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ) { - return (await this.#slots.getPubSubClient()) - .SUBSCRIBE(channels, listener, bufferMode); - } + if (!redirectTo) { + throw new Error(`Cannot find node ${address}`); + } - subscribe = this.SUBSCRIBE; - - async UNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ) { - return this.#slots.executeUnsubscribeCommand(client => - client.UNSUBSCRIBE(channels, listener, bufferMode) - ); - } + client = redirectTo; - unsubscribe = this.UNSUBSCRIBE; + const chainId = Symbol('Asking Chain'); + myOpts = options ? {...options} : {}; + myOpts.chainId = chainId; - 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) - ); - } + client.sendCommand(parseArgs(ASKING), {chainId: chainId}).catch(err => { console.log(`Asking Failed: ${err}`) } ); - 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; - } + 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, + options, + (client, opts) => client.sendCommand(args, opts) + ); + } + + 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..f370618ff30 100644 --- a/packages/client/lib/cluster/multi-command.ts +++ b/packages/client/lib/cluster/multi-command.ts @@ -1,141 +1,279 @@ -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 RedisCluster from '.'; - -type RedisClusterMultiCommandSignature< - C extends RedisCommand, - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = (...args: Parameters) => RedisClusterMultiCommandType; +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 { BasicCommandParser } from '../client/parser'; +import { Tail } from '../commands/generic-transformers'; + +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: Tail>) => 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()) - ); - } - - EXEC = this.exec; - - async execAsPipeline(): Promise> { - return this.#multi.transformReplies( - await this.#executor(this.#multi.queue, this.#firstKey) - ); - } -} + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = ( + RedisClusterMultiCommand & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +export type ClusterMultiExecute = ( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + commands: Array +) => Promise>; + +export default class RedisClusterMultiCommand { + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + + return function (this: RedisClusterMultiCommand, ...args: Array) { + 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, + redisArgs, + transformReply + ); + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { + 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, + redisArgs, + transformReply + ); + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { + 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, + redisArgs, + transformReply + ); + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); -attachCommands({ - BaseClass: RedisClusterMultiCommand, - commands: COMMANDS, - executor: RedisClusterMultiCommand.prototype.commandsExecutor -}); + return function (this: RedisClusterMultiCommand, ...args: Array) { + 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 + ); + }; + } + + 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 + }); + } + + readonly #multi: RedisMultiCommand + + readonly #executeMulti: ClusterMultiExecute; + readonly #executePipeline: ClusterMultiExecute; + #firstKey: RedisArgument | undefined; + #isReadonly: boolean | undefined = true; + + constructor( + executeMulti: ClusterMultiExecute, + executePipeline: ClusterMultiExecute, + routing: RedisArgument | undefined, + typeMapping?: TypeMapping + ) { + this.#multi = new RedisMultiCommand(typeMapping); + this.#executeMulti = executeMulti; + this.#executePipeline = executePipeline; + this.#firstKey = routing; + } + + #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; + } + + #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(); + + return this.#multi.transformReplies( + await this.#executeMulti( + this.#firstKey, + this.#isReadonly, + this.#multi.queue + ) + ) 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 + ) + ) 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..6e5a2687cb1 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, RedisArgument, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TransformReply } 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): TransformReply | undefined { + 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..09d5ecade5a 100644 --- a/packages/client/lib/commands/ACL_CAT.spec.ts +++ b/packages/client/lib/commands/ACL_CAT.spec.ts @@ -1,23 +1,32 @@ -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 { parseArgs } from './generic-transformers'; +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( + parseArgs(ACL_CAT), + ['ACL', 'CAT'] + ); + }); - it('with categoryName', () => { - assert.deepEqual( - transformArguments('dangerous'), - ['ACL', 'CAT', 'dangerous'] - ); - }); + it('with categoryName', () => { + assert.deepEqual( + parseArgs(ACL_CAT, '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..ae094b732b8 100644 --- a/packages/client/lib/commands/ACL_CAT.ts +++ b/packages/client/lib/commands/ACL_CAT.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export function transformArguments(categoryName?: RedisCommandArgument): RedisCommandArguments { - const args: RedisCommandArguments = ['ACL', 'CAT']; - +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, categoryName?: RedisArgument) { + parser.push('ACL', 'CAT'); if (categoryName) { - args.push(categoryName); + parser.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..45fa3af9fc7 100644 --- a/packages/client/lib/commands/ACL_DELUSER.spec.ts +++ b/packages/client/lib/commands/ACL_DELUSER.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ACL_DELUSER, 'username'), + ['ACL', 'DELUSER', 'username'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ACL', 'DELUSER', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ACL_DELUSER, ['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..5aa66becf75 100644 --- a/packages/client/lib/commands/ACL_DELUSER.ts +++ b/packages/client/lib/commands/ACL_DELUSER.ts @@ -1,10 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export function transformArguments( - username: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['ACL', 'DELUSER'], username); -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 3154689c29e..38a4def8361 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.spec.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ACL_DRYRUN, '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..09a51bc36f8 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.ts @@ -1,18 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const IS_READ_ONLY = true; - -export function transformArguments( - username: RedisCommandArgument, - command: Array -): RedisCommandArguments { - return [ - 'ACL', - 'DRYRUN', - username, - ...command - ]; -} - -export declare function transformReply(): RedisCommandArgument; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 3b2a022f972..35e161f424f 100644 --- a/packages/client/lib/commands/ACL_GENPASS.spec.ts +++ b/packages/client/lib/commands/ACL_GENPASS.spec.ts @@ -1,23 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL GENPASS', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'GENPASS'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ACL_GENPASS), + ['ACL', 'GENPASS'] + ); + }); - it('with bits', () => { - assert.deepEqual( - transformArguments(128), - ['ACL', 'GENPASS', '128'] - ); - }); + it('with bits', () => { + assert.deepEqual( + parseArgs(ACL_GENPASS, 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..b5caa29b9b2 100644 --- a/packages/client/lib/commands/ACL_GENPASS.ts +++ b/packages/client/lib/commands/ACL_GENPASS.ts @@ -1,13 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export function transformArguments(bits?: number): RedisCommandArguments { - const args = ['ACL', 'GENPASS']; - +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, bits?: number) { + parser.push('ACL', 'GENPASS'); if (bits) { - args.push(bits.toString()); + parser.push(bits.toString()); } + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; - return args; -} - -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..83776a3473a 100644 --- a/packages/client/lib/commands/ACL_GETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_GETUSER.spec.ts @@ -1,34 +1,35 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL GETUSER', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('username'), - ['ACL', 'GETUSER', 'username'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_GETUSER, '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..b4764ad744e 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -1,40 +1,44 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export function transformArguments(username: RedisCommandArgument): RedisCommandArguments { - return ['ACL', 'GETUSER', username]; -} +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] + ]>>], +]>; -type AclGetUserRawReply = [ - 'flags', - Array, - 'passwords', - Array, - 'commands', - RedisCommandArgument, - 'keys', - Array | RedisCommandArgument, - 'channels', - Array | RedisCommandArgument, - 'selectors' | undefined, - Array> | undefined -]; - -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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, username: RedisArgument) { + parser.push('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..0f67aaa53e9 100644 --- a/packages/client/lib/commands/ACL_LIST.spec.ts +++ b/packages/client/lib/commands/ACL_LIST.spec.ts @@ -1,14 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL LIST', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_LIST), + ['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..b5f82cf272c 100644 --- a/packages/client/lib/commands/ACL_LIST.ts +++ b/packages/client/lib/commands/ACL_LIST.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'LIST']; -} - -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 703d5eeb252..a41ce45e8a6 100644 --- a/packages/client/lib/commands/ACL_LOAD.spec.ts +++ b/packages/client/lib/commands/ACL_LOAD.spec.ts @@ -1,14 +1,15 @@ -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'; +import { parseArgs } from './generic-transformers'; -describe('ACL SAVE', () => { - testUtils.isVersionGreaterThanHook([6]); +describe('ACL LOAD', () => { + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'SAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_LOAD), + ['ACL', 'LOAD'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts index 88309102b95..dc4320b99fc 100644 --- a/packages/client/lib/commands/ACL_LOAD.ts +++ b/packages/client/lib/commands/ACL_LOAD.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'LOAD']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 a8296d31da6..7da61faca37 100644 --- a/packages/client/lib/commands/ACL_LOG.spec.ts +++ b/packages/client/lib/commands/ACL_LOG.spec.ts @@ -1,53 +1,51 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ACL_LOG), + ['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( + parseArgs(ACL_LOG, 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..4cf2722ec86 100644 --- a/packages/client/lib/commands/ACL_LOG.ts +++ b/packages/client/lib/commands/ACL_LOG.ts @@ -1,50 +1,50 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -export function transformArguments(count?: number): RedisCommandArguments { - const args = ['ACL', 'LOG']; +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] +]>>; - if (count) { - args.push(count.toString()); +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, count?: number) { + parser.push('ACL', 'LOG'); + if (count != undefined) { + parser.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..62d193a132d 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts @@ -1,14 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL LOG RESET', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LOG', 'RESET'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_LOG_RESET), + ['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..9a692129bd2 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.ts @@ -1,7 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import ACL_LOG from './ACL_LOG'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'LOG', 'RESET']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: ACL_LOG.IS_READ_ONLY, + 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 f4de312bb7a..98f7c9f183d 100644 --- a/packages/client/lib/commands/ACL_SAVE.spec.ts +++ b/packages/client/lib/commands/ACL_SAVE.spec.ts @@ -1,14 +1,15 @@ -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'; +import { parseArgs } from './generic-transformers'; -describe('ACL LOAD', () => { - testUtils.isVersionGreaterThanHook([6]); +describe('ACL SAVE', () => { + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LOAD'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_SAVE), + ['ACL', 'SAVE'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_SAVE.ts b/packages/client/lib/commands/ACL_SAVE.ts index e57cd697297..ec24522724a 100644 --- a/packages/client/lib/commands/ACL_SAVE.ts +++ b/packages/client/lib/commands/ACL_SAVE.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'SAVE']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 9c8ea8a59e0..9f39868e809 100644 --- a/packages/client/lib/commands/ACL_SETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_SETUSER.spec.ts @@ -1,23 +1,24 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ACL_SETUSER, 'username', 'allkeys'), + ['ACL', 'SETUSER', 'username', 'allkeys'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('username', ['allkeys', 'allchannels']), - ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels'] - ); - }); + it('array', () => { + assert.deepEqual( + 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 a12cc8ed24e..cad013f4d15 100644 --- a/packages/client/lib/commands/ACL_SETUSER.ts +++ b/packages/client/lib/commands/ACL_SETUSER.ts @@ -1,11 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 35e06ce8494..d897b61e4f3 100644 --- a/packages/client/lib/commands/ACL_USERS.spec.ts +++ b/packages/client/lib/commands/ACL_USERS.spec.ts @@ -1,14 +1,15 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL USERS', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'USERS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_USERS), + ['ACL', 'USERS'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_USERS.ts b/packages/client/lib/commands/ACL_USERS.ts index 7970a262e26..6ce4c6d84ef 100644 --- a/packages/client/lib/commands/ACL_USERS.ts +++ b/packages/client/lib/commands/ACL_USERS.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'USERS']; -} - -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 32eb327beea..f939c657a7a 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.spec.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.spec.ts @@ -1,14 +1,15 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ACL WHOAMI', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'WHOAMI'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ACL_WHOAMI), + ['ACL', 'WHOAMI'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_WHOAMI.ts b/packages/client/lib/commands/ACL_WHOAMI.ts index 3c41171638e..eb21a75af5f 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ACL', 'WHOAMI']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 23353866843..925c16917b9 100644 --- a/packages/client/lib/commands/APPEND.spec.ts +++ b/packages/client/lib/commands/APPEND.spec.ts @@ -1,11 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('APPEND', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['APPEND', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(APPEND, '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..18fc5c7b3aa 100644 --- a/packages/client/lib/commands/APPEND.ts +++ b/packages/client/lib/commands/APPEND.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { + parser.push('APPEND', key, value); + }, -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { - 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..7be4d25d449 100644 --- a/packages/client/lib/commands/ASKING.spec.ts +++ b/packages/client/lib/commands/ASKING.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './ASKING'; +import { strict as assert } from 'node:assert'; +import ASKING from './ASKING'; +import { parseArgs } from './generic-transformers'; describe('ASKING', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ASKING'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ASKING), + ['ASKING'] + ); + }); }); diff --git a/packages/client/lib/commands/ASKING.ts b/packages/client/lib/commands/ASKING.ts index 8a87806fe62..92ce8f72390 100644 --- a/packages/client/lib/commands/ASKING.ts +++ b/packages/client/lib/commands/ASKING.ts @@ -1,7 +1,13 @@ -import { RedisCommandArguments, RedisCommandArgument } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['ASKING']; -} +export const ASKING_CMD = 'ASKING'; -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 1907488346e..762dd24f16a 100644 --- a/packages/client/lib/commands/AUTH.spec.ts +++ b/packages/client/lib/commands/AUTH.spec.ts @@ -1,25 +1,26 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './AUTH'; +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( - transformArguments({ - password: 'password' - }), - ['AUTH', 'password'] - ); - }); + describe('transformArguments', () => { + it('password only', () => { + assert.deepEqual( + parseArgs(AUTH, { + password: 'password' + }), + ['AUTH', 'password'] + ); + }); - it('username & password', () => { - assert.deepEqual( - transformArguments({ - username: 'username', - password: 'password' - }), - ['AUTH', 'username', 'password'] - ); - }); + it('username & password', () => { + assert.deepEqual( + parseArgs(AUTH, { + 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..85b48b0026e 100644 --- a/packages/client/lib/commands/AUTH.ts +++ b/packages/client/lib/commands/AUTH.ts @@ -1,16 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, { username, password }: AuthOptions) { + parser.push('AUTH'); + if (username !== undefined) { + parser.push(username); } - - return ['AUTH', username, password]; -} - -export declare function transformReply(): RedisCommandArgument; + 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 d0e150e155b..f58ec9a5762 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.spec.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.spec.ts @@ -1,11 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('BGREWRITEAOF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['BGREWRITEAOF'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(BGREWRITEAOF), + ['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..c658f3e8529 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['BGREWRITEAOF']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 8e4de5eef5b..dcf7b815119 100644 --- a/packages/client/lib/commands/BGSAVE.spec.ts +++ b/packages/client/lib/commands/BGSAVE.spec.ts @@ -1,23 +1,33 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('BGSAVE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['BGSAVE'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(BGSAVE), + ['BGSAVE'] + ); + }); - it('with SCHEDULE', () => { - assert.deepEqual( - transformArguments({ - SCHEDULE: true - }), - ['BGSAVE', 'SCHEDULE'] - ); - }); + it('with SCHEDULE', () => { + assert.deepEqual( + parseArgs(BGSAVE, { + 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..1fd6c6b5bdb 100644 --- a/packages/client/lib/commands/BGSAVE.ts +++ b/packages/client/lib/commands/BGSAVE.ts @@ -1,17 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -interface BgSaveOptions { - SCHEDULE?: true; +export interface BgSaveOptions { + SCHEDULE?: boolean; } -export function transformArguments(options?: BgSaveOptions): RedisCommandArguments { - const args = ['BGSAVE']; - +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, options?: BgSaveOptions) { + parser.push('BGSAVE'); if (options?.SCHEDULE) { - args.push('SCHEDULE'); + parser.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..e2990472948 100644 --- a/packages/client/lib/commands/BITCOUNT.spec.ts +++ b/packages/client/lib/commands/BITCOUNT.spec.ts @@ -1,44 +1,48 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('BITCOUNT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['BITCOUNT', 'key'] - ); - }); + describe('parseCommand', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(BITCOUNT, '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( + parseArgs(BITCOUNT, '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( + parseArgs(BITCOUNT, '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..decfb754db5 100644 --- a/packages/client/lib/commands/BITCOUNT.ts +++ b/packages/client/lib/commands/BITCOUNT.ts @@ -1,33 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - const args = ['BITCOUNT', key]; - +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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); - } + if (range.mode) { + parser.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..5fcc112466b 100644 --- a/packages/client/lib/commands/BITFIELD.spec.ts +++ b/packages/client/lib/commands/BITFIELD.spec.ts @@ -1,46 +1,56 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BITFIELD, '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..f095b4cf7a6 100644 --- a/packages/client/lib/commands/BITFIELD.ts +++ b/packages/client/lib/commands/BITFIELD.ts @@ -1,80 +1,86 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +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 { - const args = ['BITFIELD', key]; +export type BitFieldRoOperations = Array< + Omit +>; + +export default { + IS_READ_ONLY: false, + 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( - 'GET', - options.encoding, - options.offset.toString() - ); - break; + switch (options.operation) { + case 'GET': + parser.push( + 'GET', + options.encoding, + options.offset.toString() + ); + break; - case 'SET': - args.push( - 'SET', - options.encoding, - options.offset.toString(), - options.value.toString() - ); - break; + case 'SET': + parser.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': + parser.push( + 'INCRBY', + options.encoding, + options.offset.toString(), + options.increment.toString() + ); + break; - case 'OVERFLOW': - args.push( - 'OVERFLOW', - options.behavior - ); - break; - } + case 'OVERFLOW': + parser.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..f2c1797412f 100644 --- a/packages/client/lib/commands/BITFIELD_RO.spec.ts +++ b/packages/client/lib/commands/BITFIELD_RO.spec.ts @@ -1,27 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; -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('parseCommand', () => { + assert.deepEqual( + parseArgs(BITFIELD_RO, '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..66001718b80 100644 --- a/packages/client/lib/commands/BITFIELD_RO.ts +++ b/packages/client/lib/commands/BITFIELD_RO.ts @@ -1,26 +1,23 @@ +import { CommandParser } from '../client/parser'; +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 { - const args = ['BITFIELD_RO', key]; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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; -} - -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..25fe48fc13c 100644 --- a/packages/client/lib/commands/BITOP.spec.ts +++ b/packages/client/lib/commands/BITOP.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BITOP, '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( + parseArgs(BITOP, '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..bb770148114 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -1,16 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, operation: BitOperations, - destKey: RedisCommandArgument, - key: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['BITOP', operation, destKey], key); -} - -export declare function transformReply(): number; + destKey: RedisArgument, + key: RedisVariadicArgument + ) { + 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 2a0758fe5da..c699deab83c 100644 --- a/packages/client/lib/commands/BITPOS.spec.ts +++ b/packages/client/lib/commands/BITPOS.spec.ts @@ -1,49 +1,46 @@ -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'; +import { parseArgs } from './generic-transformers'; 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('parseCommand', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(BITPOS, '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( + parseArgs(BITPOS, '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( + parseArgs(BITPOS, '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( + parseArgs(BITPOS, '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..57e3a63b681 100644 --- a/packages/client/lib/commands/BITPOS.ts +++ b/packages/client/lib/commands/BITPOS.ts @@ -1,32 +1,32 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, + key: RedisArgument, bit: BitValue, start?: number, end?: number, mode?: 'BYTE' | 'BIT' -): RedisCommandArguments { - const args = ['BITPOS', key, bit.toString()]; + ) { + parser.push('BITPOS'); + parser.pushKey(key); + parser.push(bit.toString()); - if (typeof start === 'number') { - args.push(start.toString()); + if (start !== undefined) { + parser.push(start.toString()); } - if (typeof end === 'number') { - args.push(end.toString()); + if (end !== undefined) { + parser.push(end.toString()); } if (mode) { - args.push(mode); + parser.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..d4e9e024a8c 100644 --- a/packages/client/lib/commands/BLMOVE.spec.ts +++ b/packages/client/lib/commands/BLMOVE.spec.ts @@ -1,43 +1,36 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BLMOVE, '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..b0ada7cdb20 100644 --- a/packages/client/lib/commands/BLMOVE.ts +++ b/packages/client/lib/commands/BLMOVE.ts @@ -1,23 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + source: RedisArgument, + destination: RedisArgument, + sourceSide: ListSide, + destinationSide: ListSide, timeout: number -): RedisCommandArguments { - return [ - 'BLMOVE', - source, - destination, - sourceDirection, - destinationDirection, - timeout.toString() - ]; -} - -export declare function transformReply(): RedisCommandArgument | null; + ) { + 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 15853a771b0..6cda524b50f 100644 --- a/packages/client/lib/commands/BLMPOP.spec.ts +++ b/packages/client/lib/commands/BLMPOP.spec.ts @@ -1,32 +1,50 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BLMPOP, 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( + parseArgs(BLMPOP, 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..15d03f8d822 100644 --- a/packages/client/lib/commands/BLMPOP.ts +++ b/packages/client/lib/commands/BLMPOP.ts @@ -1,20 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { Command } from '../RESP/types'; +import LMPOP, { LMPopArguments, parseLMPopArguments } from './LMPOP'; -export const FIRST_KEY_INDEX = 3; - -export function transformArguments( - timeout: number, - keys: RedisCommandArgument | Array, - side: ListSide, - options?: LMPopOptions -): RedisCommandArguments { - return transformLMPopArguments( - ['BLMPOP', timeout.toString()], - keys, - side, - options - ); -} - -export { transformReply } from './LMPOP'; +export default { + IS_READ_ONLY: false, + 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 84920c851e1..1bb53a774b7 100644 --- a/packages/client/lib/commands/BLPOP.spec.ts +++ b/packages/client/lib/commands/BLPOP.spec.ts @@ -1,79 +1,47 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BLPOP, '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( + parseArgs(BLPOP, ['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..aa0b30e768e 100644 --- a/packages/client/lib/commands/BLPOP.ts +++ b/packages/client/lib/commands/BLPOP.ts @@ -1,31 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - keys: RedisCommandArgument | Array, - timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BLPOP'], keys); - - args.push(timeout.toString()); - - return args; -} - -type BLPopRawReply = null | [RedisCommandArgument, RedisCommandArgument]; - -type BLPopReply = null | { - key: RedisCommandArgument; - element: RedisCommandArgument; -}; - -export function transformReply(reply: BLPopRawReply): BLPopReply { +import { CommandParser } from '../client/parser'; +import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + 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; 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..de23bb34a92 100644 --- a/packages/client/lib/commands/BRPOP.spec.ts +++ b/packages/client/lib/commands/BRPOP.spec.ts @@ -1,79 +1,47 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BRPOP, '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( + parseArgs(BRPOP, ['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..401a951556d 100644 --- a/packages/client/lib/commands/BRPOP.ts +++ b/packages/client/lib/commands/BRPOP.ts @@ -1,17 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array, - timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BRPOP'], key); - - args.push(timeout.toString()); - - return args; -} - -export { transformReply } from './BLPOP'; +import { CommandParser } from '../client/parser'; +import { Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; +import BLPOP from './BLPOP'; + +export default { + IS_READ_ONLY: true, + 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 214af4553ac..6c2a2a2c900 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.spec.ts @@ -1,47 +1,43 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('BRPOPLPUSH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 0), - ['BRPOPLPUSH', 'source', 'destination', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(BRPOPLPUSH, '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..72f63a1c1e5 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - timeout: number -): RedisCommandArguments { - return ['BRPOPLPUSH', source, destination, timeout.toString()]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + IS_READ_ONLY: false, + 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 0e381c114f2..8b082a214ee 100644 --- a/packages/client/lib/commands/BZMPOP.spec.ts +++ b/packages/client/lib/commands/BZMPOP.spec.ts @@ -1,32 +1,56 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BZMPOP, 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( + parseArgs(BZMPOP, 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..98079b7a20d 100644 --- a/packages/client/lib/commands/BZMPOP.ts +++ b/packages/client/lib/commands/BZMPOP.ts @@ -1,20 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { SortedSetSide, transformZMPopArguments, ZMPopOptions } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { Command } from '../RESP/types'; +import ZMPOP, { parseZMPopArguments, ZMPopArguments } 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 { + IS_READ_ONLY: false, + 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 d5c17437122..fbf60862327 100644 --- a/packages/client/lib/commands/BZPOPMAX.spec.ts +++ b/packages/client/lib/commands/BZPOPMAX.spec.ts @@ -1,65 +1,52 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BZPOPMAX, '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( + parseArgs(BZPOPMAX, ['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..1a5159269e9 100644 --- a/packages/client/lib/commands/BZPOPMAX.ts +++ b/packages/client/lib/commands/BZPOPMAX.ts @@ -1,29 +1,33 @@ -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; -} - -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 { +import { CommandParser } from '../client/parser'; +import { NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, transformDoubleReply } from './generic-transformers'; + +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { + parser.push('BZPOPMAX'); + parser.pushKeys(keys); + parser.push(timeout.toString()); + }, + 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..2f8cab8dedf 100644 --- a/packages/client/lib/commands/BZPOPMIN.spec.ts +++ b/packages/client/lib/commands/BZPOPMIN.spec.ts @@ -1,65 +1,52 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(BZPOPMIN, '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( + parseArgs(BZPOPMIN, ['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..9dc4c47e13d 100644 --- a/packages/client/lib/commands/BZPOPMIN.ts +++ b/packages/client/lib/commands/BZPOPMIN.ts @@ -1,17 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; +import BZPOPMAX from './BZPOPMAX'; + +export default { + IS_READ_ONLY: BZPOPMAX.IS_READ_ONLY, + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { + parser.push('BZPOPMIN'); + parser.pushKeys(keys); + parser.push(timeout.toString()); + }, + 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..ad3511b3e97 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.spec.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.spec.ts @@ -1,20 +1,21 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_CACHING'; +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( - transformArguments(true), - ['CLIENT', 'CACHING', 'YES'] - ); - }); + describe('transformArguments', () => { + it('true', () => { + assert.deepEqual( + parseArgs(CLIENT_CACHING, true), + ['CLIENT', 'CACHING', 'YES'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'CACHING', 'NO'] - ); - }); + it('false', () => { + assert.deepEqual( + 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 bc2fbe41e9d..9987e49c99b 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.ts @@ -1,11 +1,15 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { - return [ - 'CLIENT', - 'CACHING', - value ? 'YES' : 'NO' - ]; -} - -export declare function transformReply(): 'OK' | Buffer; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 0a09713882f..5b0dfdb8437 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts @@ -1,11 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT GETNAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'GETNAME'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_GETNAME), + ['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..2e18c43cd5f 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.ts @@ -1,7 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, NullReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'GETNAME']; -} - -export declare function transformReply(): string | null; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 09dd9677e32..a7c375fec26 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_GETREDIR'; +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( - transformArguments(), - ['CLIENT', 'GETREDIR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_GETREDIR), + ['CLIENT', 'GETREDIR'] + ); + }); }); diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.ts b/packages/client/lib/commands/CLIENT_GETREDIR.ts index d192adf284a..80cc6418dab 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.ts @@ -1,7 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'GETREDIR']; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 6792a8c31be..51b308adf2c 100644 --- a/packages/client/lib/commands/CLIENT_ID.spec.ts +++ b/packages/client/lib/commands/CLIENT_ID.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT ID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'ID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_ID), + ['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..da58786ec3c 100644 --- a/packages/client/lib/commands/CLIENT_ID.ts +++ b/packages/client/lib/commands/CLIENT_ID.ts @@ -1,7 +1,11 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['CLIENT', 'ID']; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 ccb99017cf3..50345a46ce3 100644 --- a/packages/client/lib/commands/CLIENT_INFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts @@ -1,50 +1,51 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT INFO', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'INFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_INFO), + ['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..36dac175443 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -1,94 +1,117 @@ -export const IS_READ_ONLY = true; - -export function transformArguments(): Array { - return ['CLIENT', 'INFO']; -} +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..5078a267516 100644 --- a/packages/client/lib/commands/CLIENT_KILL.spec.ts +++ b/packages/client/lib/commands/CLIENT_KILL.spec.ts @@ -1,120 +1,121 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLIENT_KILL, { + filter: CLIENT_KILL_FILTERS.ADDRESS, + address: 'ip:6379' + }), + ['CLIENT', 'KILL', 'ADDR', 'ip:6379'] + ); + }); + + it('LOCAL_ADDRESS', () => { + assert.deepEqual( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, { + 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( + parseArgs(CLIENT_KILL, [ + { + 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..24f8f0873f1 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 { CommandParser } from '../client/parser'; +import { 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 { - const args = ['CLIENT', 'KILL']; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, filters: ClientKillFilter | Array) { + parser.push('CLIENT', 'KILL'); if (Array.isArray(filters)) { - for (const filter of filters) { - pushFilter(args, filter); - } + for (const filter of filters) { + pushFilter(parser, filter); + } } else { - pushFilter(args, filters); + pushFilter(parser, filters); } - return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; + +function pushFilter(parser: CommandParser, filter: ClientKillFilter): void { + if (filter === CLIENT_KILL_FILTERS.SKIP_ME) { + parser.push('SKIPME'); + return; + } + + parser.push(filter.filter); + + switch (filter.filter) { + case CLIENT_KILL_FILTERS.ADDRESS: + parser.push(filter.address); + break; + + case CLIENT_KILL_FILTERS.LOCAL_ADDRESS: + parser.push(filter.localAddress); + break; + + case CLIENT_KILL_FILTERS.ID: + parser.push( + typeof filter.id === 'number' ? + filter.id.toString() : + filter.id + ); + break; + + case CLIENT_KILL_FILTERS.TYPE: + parser.push(filter.type); + break; + + case CLIENT_KILL_FILTERS.USER: + parser.push(filter.username); + break; + + case CLIENT_KILL_FILTERS.SKIP_ME: + parser.push(filter.skipMe ? 'yes' : 'no'); + break; + + case CLIENT_KILL_FILTERS.MAXAGE: + parser.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..34709c5f14f 100644 --- a/packages/client/lib/commands/CLIENT_LIST.spec.ts +++ b/packages/client/lib/commands/CLIENT_LIST.spec.ts @@ -1,78 +1,78 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLIENT_LIST), + ['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( + parseArgs(CLIENT_LIST, { + 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( + parseArgs(CLIENT_LIST, { + 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..1e7f3d9ab40 100644 --- a/packages/client/lib/commands/CLIENT_LIST.ts +++ b/packages/client/lib/commands/CLIENT_LIST.ts @@ -1,43 +1,41 @@ -import { RedisCommandArguments, RedisCommandArgument } from '.'; -import { pushVerdictArguments } from './generic-transformers'; -import { transformReply as transformClientInfoReply, ClientInfoReply } from './CLIENT_INFO'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, filter?: ListFilter) { + parser.push('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) { + parser.push('TYPE', filter.TYPE); + } else { + parser.push('ID'); + parser.pushVariadic(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..50afd413492 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLIENT_NO_EVICT, true), + ['CLIENT', 'NO-EVICT', 'ON'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'NO-EVICT', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + parseArgs(CLIENT_NO_EVICT, 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..de2f65270e2 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts @@ -1,11 +1,15 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { - return [ - 'CLIENT', - 'NO-EVICT', - value ? 'ON' : 'OFF' - ]; -} - -export declare function transformReply(): 'OK' | Buffer; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 80ee0ada1fd..ec5c9f18ae9 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts @@ -1,30 +1,31 @@ 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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLIENT_NO_TOUCH, true), + ['CLIENT', 'NO-TOUCH', 'ON'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'NO-TOUCH', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + parseArgs(CLIENT_NO_TOUCH, 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..8c6deff4af5 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts @@ -1,11 +1,16 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { - return [ - 'CLIENT', - 'NO-TOUCH', - value ? 'ON' : 'OFF' - ]; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, value: boolean) { + parser.push( + '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..e213433afbe 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT PAUSE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0), - ['CLIENT', 'PAUSE', '0'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(CLIENT_PAUSE, 0), + ['CLIENT', 'PAUSE', '0'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments(0, 'ALL'), - ['CLIENT', 'PAUSE', '0', 'ALL'] - ); - }); + it('with mode', () => { + assert.deepEqual( + parseArgs(CLIENT_PAUSE, 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..ae6e4376364 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.ts @@ -1,20 +1,14 @@ -import { RedisCommandArguments } from '.'; - -export function transformArguments( - timeout: number, - mode?: 'WRITE' | 'ALL' -): RedisCommandArguments { - const args = [ - 'CLIENT', - 'PAUSE', - timeout.toString() - ]; - +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, timeout: number, mode?: 'WRITE' | 'ALL') { + parser.push('CLIENT', 'PAUSE', timeout.toString()); if (mode) { - args.push(mode); + parser.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..b2b339c3d19 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts @@ -1,11 +1,21 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT SETNAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('name'), - ['CLIENT', 'SETNAME', 'name'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_SETNAME, '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..335891e8308 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(name: RedisCommandArgument): RedisCommandArguments { - return ['CLIENT', 'SETNAME', name]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 bbd0b13e777..032725635ee 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts @@ -1,101 +1,102 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLIENT_TRACKING, true), + ['CLIENT', 'TRACKING', 'ON'] + ); + }); - it('with REDIRECT', () => { - assert.deepEqual( - transformArguments(true, { - REDIRECT: 1 - }), - ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1'] - ); - }); + it('with REDIRECT', () => { + assert.deepEqual( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, 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( + parseArgs(CLIENT_TRACKING, true, { + NOLOOP: true + }), + ['CLIENT', 'TRACKING', 'ON', 'NOLOOP'] + ); + }); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'TRACKING', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + parseArgs(CLIENT_TRACKING, 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..df70a3705f9 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 { CommandParser } from '../client/parser'; +import { 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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, mode: M, - options?: M extends true ? ClientTrackingOptions : undefined -): RedisCommandArguments { - const args: RedisCommandArguments = [ - 'CLIENT', - 'TRACKING', - mode ? 'ON' : 'OFF' - ]; + options?: M extends true ? ClientTrackingOptions : never + ) { + parser.push( + 'CLIENT', + 'TRACKING', + mode ? 'ON' : 'OFF' + ); if (mode) { - if (options?.REDIRECT) { - args.push( - 'REDIRECT', - options.REDIRECT.toString() - ); - } + if (options?.REDIRECT) { + parser.push( + 'REDIRECT', + options.REDIRECT.toString() + ); + } - if (isBroadcast(options)) { - args.push('BCAST'); + if (isBroadcast(options)) { + parser.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) { + parser.push('PREFIX', prefix); } - } else if (isOptIn(options)) { - args.push('OPTIN'); - } else if (isOptOut(options)) { - args.push('OPTOUT'); + } else { + parser.push('PREFIX', options.PREFIX); + } } + } else if (isOptIn(options)) { + parser.push('OPTIN'); + } else if (isOptOut(options)) { + parser.push('OPTOUT'); + } - if (options?.NOLOOP) { - args.push('NOLOOP'); - } + if (options?.NOLOOP) { + parser.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..d776519df22 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts @@ -1,25 +1,26 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT TRACKINGINFO', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'TRACKINGINFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_TRACKINGINFO), + ['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..fe6e090455c 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts @@ -1,28 +1,24 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { TuplesToMapReply, BlobStringReply, SetReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'TRACKINGINFO']; -} +type TrackingInfo = TuplesToMapReply<[ + [BlobStringReply<'flags'>, SetReply], + [BlobStringReply<'redirect'>, NumberReply], + [BlobStringReply<'prefixes'>, ArrayReply] +]>; -type RawReply = [ - 'flags', - Array, - 'redirect', - number, - 'prefixes', - Array -]; - -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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..0b58cf6517e 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLIENT UNPAUSE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'UNPAUSE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLIENT_UNPAUSE), + ['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..c202e50a5df 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts @@ -1,7 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'UNPAUSE']; -} - -export declare function transformReply(): 'OK' | Buffer; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 c16476de436..4a9b1839bb4 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts @@ -1,20 +1,21 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_ADDSLOTS'; +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( - transformArguments(0), - ['CLUSTER', 'ADDSLOTS', '0'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + parseArgs(CLUSTER_ADDSLOTS, 0), + ['CLUSTER', 'ADDSLOTS', '0'] + ); + }); - it('multiple', () => { - assert.deepEqual( - transformArguments([0, 1]), - ['CLUSTER', 'ADDSLOTS', '0', '1'] - ); - }); + it('multiple', () => { + assert.deepEqual( + 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 6cd357fb823..0f5c4513d1d 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts @@ -1,11 +1,12 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictNumberArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(slots: number | Array): RedisCommandArguments { - return pushVerdictNumberArguments( - ['CLUSTER', 'ADDSLOTS'], - slots - ); -} - -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 ebd1e3445ff..40706968f93 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts @@ -1,29 +1,33 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLUSTER_ADDSLOTSRANGE, { + start: 0, + end: 1 + }), + ['CLUSTER', 'ADDSLOTSRANGE', '0', '1'] + ); }); + + it('multiple', () => { + assert.deepEqual( + parseArgs(CLUSTER_ADDSLOTSRANGE, [{ + 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..40780731981 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts @@ -1,13 +1,13 @@ -import { RedisCommandArguments } from '.'; -import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; -export function transformArguments( - ranges: SlotRange | Array -): RedisCommandArguments { - return pushSlotRangesArguments( - ['CLUSTER', 'ADDSLOTSRANGE'], - ranges - ); -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 edb68b3b3b0..f3ecde9f6a8 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.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 { transformArguments } from './CLUSTER_BUMPEPOCH'; +import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER BUMPEPOCH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'BUMPEPOCH'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_BUMPEPOCH), + ['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..04b62f85424 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'BUMPEPOCH']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'BUMPED' | 'STILL'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 558110d0a28..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,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 './CLUSTER_COUNT-FAILURE-REPORTS'; +import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER COUNT-FAILURE-REPORTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_COUNT_FAILURE_REPORTS, '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..0ac311f7ecd 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts @@ -1,5 +1,11 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId]; -} +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 27ecbcfffa3..52848409465 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.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 { transformArguments } from './CLUSTER_COUNTKEYSINSLOT'; +import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER COUNTKEYSINSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'COUNTKEYSINSLOT', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_COUNTKEYSINSLOT, 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..63b4a8e02e2 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts @@ -1,5 +1,11 @@ -export function transformArguments(slot: number): Array { - return ['CLUSTER', 'COUNTKEYSINSLOT', slot.toString()]; -} +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 85d13f4ed3d..2937fdd4d79 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts @@ -1,20 +1,21 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_DELSLOTS'; +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( - transformArguments(0), - ['CLUSTER', 'DELSLOTS', '0'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + parseArgs(CLUSTER_DELSLOTS, 0), + ['CLUSTER', 'DELSLOTS', '0'] + ); + }); - it('multiple', () => { - assert.deepEqual( - transformArguments([0, 1]), - ['CLUSTER', 'DELSLOTS', '0', '1'] - ); - }); + it('multiple', () => { + assert.deepEqual( + 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 bf8d9c18900..9be6e962a18 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts @@ -1,11 +1,12 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictNumberArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(slots: number | Array): RedisCommandArguments { - return pushVerdictNumberArguments( - ['CLUSTER', 'DELSLOTS'], - slots - ); -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 8fd50d01a54..6007421d11b 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts @@ -1,29 +1,30 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_DELSLOTSRANGE'; +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( - transformArguments({ - start: 0, - end: 1 - }), - ['CLUSTER', 'DELSLOTSRANGE', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + parseArgs(CLUSTER_DELSLOTSRANGE, { + 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( + parseArgs(CLUSTER_DELSLOTSRANGE, [{ + 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..64c04021ba1 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts @@ -1,13 +1,13 @@ -import { RedisCommandArguments } from '.'; -import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; -export function transformArguments( - ranges: SlotRange | Array -): RedisCommandArguments { - return pushSlotRangesArguments( - ['CLUSTER', 'DELSLOTSRANGE'], - ranges - ); -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 578ff56b9cd..f8e4b986048 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts @@ -1,20 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CLUSTER_FAILOVER), + ['CLUSTER', 'FAILOVER'] + ); }); + + it('with mode', () => { + assert.deepEqual( + 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 9bc4b69f343..f74d65bd691 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts @@ -1,16 +1,26 @@ -export enum FailoverModes { - FORCE = 'FORCE', - TAKEOVER = 'TAKEOVER' -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(mode?: FailoverModes): Array { - const args = ['CLUSTER', 'FAILOVER']; +export const FAILOVER_MODES = { + FORCE: 'FORCE', + TAKEOVER: 'TAKEOVER' +} as const; - if (mode) { - args.push(mode); - } +export type FailoverMode = typeof FAILOVER_MODES[keyof typeof FAILOVER_MODES]; - return args; +export interface ClusterFailoverOptions { + mode?: FailoverMode; } -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser:CommandParser, options?: ClusterFailoverOptions) { + parser.push('CLUSTER', 'FAILOVER'); + + if (options?.mode) { + parser.push(options.mode); + } + }, + 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..43701adfe6a 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_FLUSHSLOTS'; +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( - transformArguments(), - ['CLUSTER', 'FLUSHSLOTS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_FLUSHSLOTS), + ['CLUSTER', 'FLUSHSLOTS'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts index dfb1e1ccde8..dab22b2e740 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'FLUSHSLOTS']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 cadcdb678f3..8d02374cf87 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_FORGET'; +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( - transformArguments('0'), - ['CLUSTER', 'FORGET', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + 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 fc557073aeb..2928c3e9075 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.ts @@ -1,5 +1,11 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'FORGET', nodeId]; -} +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 957b7de20cb..468eecc74a9 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts @@ -1,21 +1,26 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER GETKEYSINSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 10), - ['CLUSTER', 'GETKEYSINSLOT', '0', '10'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_GETKEYSINSLOT, 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..2fd630ea1af 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -1,5 +1,11 @@ -export function transformArguments(slot: number, count: number): Array { - return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()]; -} +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 69d5c4a8c56..01dafce8d53 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.spec.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.spec.ts @@ -1,55 +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 './CLUSTER_INFO'; +import CLUSTER_INFO from './CLUSTER_INFO'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'INFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_INFO), + ['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..53140b38819 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -1,47 +1,11 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'INFO']; -} - -interface ClusterInfoReply { - state: string; - slots: { - assigned: number; - ok: number; - pfail: number; - fail: number; - }; - knownNodes: number; - size: number; - currentEpoch: number; - myEpoch: number; - stats: { - messagesSent: number; - messagesReceived: number; - }; -} - -export function transformReply(reply: string): ClusterInfoReply { - const lines = reply.split('\r\n'); - - return { - state: extractLineValue(lines[0]), - slots: { - assigned: Number(extractLineValue(lines[1])), - ok: Number(extractLineValue(lines[2])), - pfail: Number(extractLineValue(lines[3])), - fail: Number(extractLineValue(lines[4])) - }, - knownNodes: Number(extractLineValue(lines[5])), - size: Number(extractLineValue(lines[6])), - currentEpoch: Number(extractLineValue(lines[7])), - myEpoch: Number(extractLineValue(lines[8])), - stats: { - messagesSent: Number(extractLineValue(lines[9])), - messagesReceived: Number(extractLineValue(lines[10])) - } - }; -} - -export function extractLineValue(line: string): string { - return line.substring(line.indexOf(':') + 1); -} +import { CommandParser } from '../client/parser'; +import { VerbatimStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 3bbc9f9cb2d..188c403abb5 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.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 { transformArguments } from './CLUSTER_KEYSLOT'; +import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER KEYSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['CLUSTER', 'KEYSLOT', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_KEYSLOT, '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..d81a14e1a96 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts @@ -1,5 +1,11 @@ -export function transformArguments(key: string): Array { - return ['CLUSTER', 'KEYSLOT', key]; -} +import { CommandParser } from '../client/parser'; +import { Command, NumberReply, RedisArgument } from '../RESP/types'; -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 982973e8ea5..609ecfd3da9 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER LINKS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'LINKS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_LINKS), + ['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..e98f61e762b 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.ts @@ -1,38 +1,33 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'LINKS']; -} +import { CommandParser } from '../client/parser'; +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -type ClusterLinksRawReply = Array<[ - 'direction', - string, - 'node', - string, - 'createTime', - number, - 'events', - string, - 'send-buffer-allocated', - number, - 'send-buffer-used', - 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], +]>>; -type ClusterLinksReply = Array<{ - direction: string; - node: string; - createTime: number; - events: string; - sendBufferAllocated: number; - sendBufferUsed: number; -}>; - -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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..6c063f34e45 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_MEET'; +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( - transformArguments('127.0.0.1', 6379), - ['CLUSTER', 'MEET', '127.0.0.1', '6379'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + 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 e6ce1c1fce4..804e5963d19 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.ts @@ -1,5 +1,11 @@ -export function transformArguments(ip: string, port: number): Array { - return ['CLUSTER', 'MEET', ip, port.toString()]; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 f427d7058e2..78bb4495e3c 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER MYID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'MYID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_MYID), + ['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..2aae7cdd8e0 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'MYID']; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 180289870ca..6c2a61801bc 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts @@ -1,22 +1,23 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_MYSHARDID'; +import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER MYSHARDID', () => { - testUtils.isVersionGreaterThanHook([7, 2]); + testUtils.isVersionGreaterThanHook([7, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'MYSHARDID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_MYSHARDID), + ['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..ccde3ee249b 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts @@ -1,7 +1,12 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments() { - return ['CLUSTER', 'MYSHARDID']; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..a49996586b7 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.spec.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.spec.ts @@ -1,145 +1,21 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER NODES', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'NODES'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_NODES), + ['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..c8b59f88224 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -1,105 +1,11 @@ -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>(); - - 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)) - }; -} +import { CommandParser } from '../client/parser'; +import { VerbatimStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 6c902dc0d82..11bf086bb66 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts @@ -1,11 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER REPLICAS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'REPLICAS', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_REPLICAS, '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..eb60e560b45 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts @@ -1,5 +1,11 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'REPLICAS', nodeId]; -} +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export { transformReply } from './CLUSTER_NODES'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 926b7dd0a77..3f130d360bf 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_REPLICATE'; +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( - transformArguments('0'), - ['CLUSTER', 'REPLICATE', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + 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 c74e1ec5960..d7312ae108e 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.ts @@ -1,5 +1,11 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'REPLICATE', nodeId]; -} +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 340da7457c1..1ef55e3f572 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.spec.ts @@ -1,20 +1,23 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_RESET'; +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( - transformArguments(), - ['CLUSTER', 'RESET'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(CLUSTER_RESET), + ['CLUSTER', 'RESET'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('HARD'), - ['CLUSTER', 'RESET', 'HARD'] - ); - }); + it('with mode', () => { + assert.deepEqual( + 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 c6901e045dc..2ba1a6eaf20 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.ts @@ -1,11 +1,19 @@ -export function transformArguments(mode?: 'HARD' | 'SOFT'): Array { - const args = ['CLUSTER', 'RESET']; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; - if (mode) { - args.push(mode); - } - - return args; +export interface ClusterResetOptions { + mode?: 'HARD' | 'SOFT'; } -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, options?: ClusterResetOptions) { + parser.push('CLUSTER', 'RESET'); + + if (options?.mode) { + parser.push(options.mode); + } + }, + 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..a0d317ffae4 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.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 { transformArguments } from './CLUSTER_SAVECONFIG'; +import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SAVECONFIG', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'SAVECONFIG'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_SAVECONFIG), + ['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..08da2a45b89 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts @@ -1,5 +1,12 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'SAVECONFIG']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..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,11 +1,12 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SET-CONFIG-EPOCH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'SET-CONFIG-EPOCH', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + 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 c50a6b9d3a5..ba423df7fb7 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts @@ -1,5 +1,11 @@ -export function transformArguments(configEpoch: number): Array { - return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()]; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 0f46aafd13e..fac496c3afb 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts @@ -1,20 +1,21 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SETSLOT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0, ClusterSlotStates.IMPORTING), - ['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(CLUSTER_SETSLOT, 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( + 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 c01505c71a3..1f74316a3f3 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.ts @@ -1,22 +1,24 @@ -export enum ClusterSlotStates { - IMPORTING = 'IMPORTING', - MIGRATING = 'MIGRATING', - STABLE = 'STABLE', - NODE = 'NODE' -} +import { CommandParser } from '../client/parser'; +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; - if (nodeId) { - args.push(nodeId); - } +export type ClusterSlotState = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES]; - return args; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, slot: number, state: ClusterSlotState, nodeId?: RedisArgument) { + parser.push('CLUSTER', 'SETSLOT', slot.toString(), state); -export declare function transformReply(): 'OK'; + if (nodeId) { + parser.push(nodeId); + } + }, + 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..28879b036ae 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts @@ -1,76 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SLOTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'SLOTS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CLUSTER_SLOTS), + ['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..f6f967abe28 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.ts @@ -1,46 +1,42 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..52eb7eb2fea 100644 --- a/packages/client/lib/commands/COMMAND.ts +++ b/packages/client/lib/commands/COMMAND.ts @@ -1,12 +1,15 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { - return ['COMMAND']; -} - -export function transformReply(reply: Array): Array { +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 { 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..a36091df482 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.spec.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('COMMAND COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['COMMAND', 'COUNT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COMMAND_COUNT), + ['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..ef561920b0b 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.ts @@ -1,9 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { - return ['COMMAND', 'COUNT']; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 a92d032c5d6..332e2d51fbd 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('COMMAND GETKEYS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['GET', 'key']), - ['COMMAND', 'GETKEYS', 'GET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COMMAND_GETKEYS, ['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..97c5cb69ce2 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts @@ -1,9 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(args: Array): RedisCommandArguments { - return ['COMMAND', 'GETKEYS', ...args]; -} - -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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.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..72c1e16a2d1 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts @@ -1,24 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - 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]) => ({ +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, args: Array) { + parser.push('COMMAND', 'GETKEYSANDFLAGS'); + parser.push(...args); + }, + 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..fdf03780652 100644 --- a/packages/client/lib/commands/COMMAND_INFO.ts +++ b/packages/client/lib/commands/COMMAND_INFO.ts @@ -1,12 +1,15 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - return ['COMMAND', 'INFO', ...commands]; -} - -export function transformReply(reply: Array): Array { +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 { 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..d2ee9e66161 100644 --- a/packages/client/lib/commands/COMMAND_LIST.spec.ts +++ b/packages/client/lib/commands/COMMAND_LIST.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, FilterBy } from './COMMAND_LIST'; +import COMMAND_LIST from './COMMAND_LIST'; +import { parseArgs } from './generic-transformers'; describe('COMMAND LIST', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['COMMAND', 'LIST'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(COMMAND_LIST), + ['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( + parseArgs(COMMAND_LIST, { + 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( + parseArgs(COMMAND_LIST, { + 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( + parseArgs(COMMAND_LIST, { + 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..ba518b70eca 100644 --- a/packages/client/lib/commands/COMMAND_LIST.ts +++ b/packages/client/lib/commands/COMMAND_LIST.ts @@ -1,31 +1,34 @@ -import { RedisCommandArguments } from '.'; - -export const IS_READ_ONLY = true; - -export enum FilterBy { - MODULE = 'MODULE', - ACLCAT = 'ACLCAT', - PATTERN = 'PATTERN' +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; + +export const COMMAND_LIST_FILTER_BY = { + MODULE: 'MODULE', + ACLCAT: 'ACLCAT', + PATTERN: 'PATTERN' +} as const; + +export type CommandListFilterBy = typeof COMMAND_LIST_FILTER_BY[keyof typeof COMMAND_LIST_FILTER_BY]; + +export interface CommandListOptions { + FILTERBY?: { + type: CommandListFilterBy; + value: RedisArgument; + }; } -interface Filter { - filterBy: FilterBy; - value: string; -} - - -export function transformArguments(filter?: Filter): RedisCommandArguments { - const args = ['COMMAND', 'LIST']; - - if (filter) { - args.push( - 'FILTERBY', - filter.filterBy, - filter.value - ); +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, options?: CommandListOptions) { + parser.push('COMMAND', 'LIST'); + + if (options?.FILTERBY) { + parser.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..c3f0eac76dd 100644 --- a/packages/client/lib/commands/CONFIG_GET.spec.ts +++ b/packages/client/lib/commands/CONFIG_GET.spec.ts @@ -1,11 +1,60 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('*'), - ['CONFIG', 'GET', '*'] - ); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(CONFIG_GET, '*'), + ['CONFIG', 'GET', '*'] + ); }); + + it('Array', () => { + assert.deepEqual( + parseArgs(CONFIG_GET, ['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); + + 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_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index 3afc0eddfd0..54fa997bf61 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -1,5 +1,16 @@ -export function transformArguments(parameter: string): Array { - return ['CONFIG', 'GET', parameter]; -} +import { CommandParser } from '../client/parser'; +import { MapReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, transformTuplesReply } from './generic-transformers'; -export { transformTuplesReply as transformReply } from './generic-transformers'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, parameters: RedisVariadicArgument) { + parser.push('CONFIG', 'GET'); + parser.pushVariadic(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..f2f573df0dc 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_RESETSTAT'; +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( - transformArguments(), - ['CONFIG', 'RESETSTAT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CONFIG_RESETSTAT), + ['CONFIG', 'RESETSTAT'] + ); + }); }); diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts index aba54bc3c7b..15de5ba7808 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CONFIG', 'RESETSTAT']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 cbc3e5b59d8..bc006e84c80 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_REWRITE'; +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( - transformArguments(), - ['CONFIG', 'REWRITE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CONFIG_REWRITE), + ['CONFIG', 'REWRITE'] + ); + }); }); diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts index 67984adf300..ae6712ffb57 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CONFIG', 'REWRITE']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 93a7a6ff25e..f9f34dec937 100644 --- a/packages/client/lib/commands/CONFIG_SET.spec.ts +++ b/packages/client/lib/commands/CONFIG_SET.spec.ts @@ -1,24 +1,42 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(CONFIG_SET, '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( + parseArgs(CONFIG_SET, { + 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); + + 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/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts index 41f40d035d2..dd1bbc29ef2 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 { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, ...[parameterOrConfig, value]: SingleParameter | MultipleParameters -): RedisCommandArguments { - const args: RedisCommandArguments = ['CONFIG', 'SET']; - - if (typeof parameterOrConfig === 'string') { - args.push(parameterOrConfig, value!); + ) { + parser.push('CONFIG', 'SET'); + + if (typeof parameterOrConfig === 'string' || parameterOrConfig instanceof Buffer) { + parser.push(parameterOrConfig, value!); } else { - for (const [key, value] of Object.entries(parameterOrConfig)) { - args.push(key, value); - } + for (const [key, value] of Object.entries(parameterOrConfig)) { + parser.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..cd0c6ec9fbe 100644 --- a/packages/client/lib/commands/COPY.spec.ts +++ b/packages/client/lib/commands/COPY.spec.ts @@ -1,67 +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 './COPY'; +import COPY from './COPY'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(COPY, 'source', 'destination'), + ['COPY', 'source', 'destination'] + ); }); - describe('transformReply', () => { - it('0', () => { - assert.equal( - transformReply(0), - false - ); - }); + it('with destination DB flag', () => { + assert.deepEqual( + parseArgs(COPY, 'source', 'destination', { + DB: 1 + }), + ['COPY', 'source', 'destination', 'DB', '1'] + ); + }); - it('1', () => { - assert.equal( - transformReply(1), - true - ); - }); + it('with replace flag', () => { + assert.deepEqual( + parseArgs(COPY, '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( + parseArgs(COPY, '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..f26a930264c 100644 --- a/packages/client/lib/commands/COPY.ts +++ b/packages/client/lib/commands/COPY.ts @@ -1,28 +1,24 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { + parser.push('COPY'); + parser.pushKeys([source, destination]); -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - options?: CopyCommandOptions -): RedisCommandArguments { - const args = ['COPY', source, destination]; - - if (options?.destinationDb) { - args.push('DB', options.destinationDb.toString()); + if (options?.DB) { + parser.push('DB', options.DB.toString()); } - if (options?.replace) { - args.push('REPLACE'); + if (options?.REPLACE) { + parser.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..5778e30de3e 100644 --- a/packages/client/lib/commands/DBSIZE.spec.ts +++ b/packages/client/lib/commands/DBSIZE.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('DBSIZE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['DBSIZE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DBSIZE), + ['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..1ba1f060476 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -1,7 +1,11 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['DBSIZE']; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 75e1205feda..69ff5a5391f 100644 --- a/packages/client/lib/commands/DECR.spec.ts +++ b/packages/client/lib/commands/DECR.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('DECR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['DECR', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DECR, '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..b9a6200d81b 100644 --- a/packages/client/lib/commands/DECR.ts +++ b/packages/client/lib/commands/DECR.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['DECR', key]; -} - -export declare function transformReply(): number; +export default { + 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 d2c23e94728..ae80fd714e0 100644 --- a/packages/client/lib/commands/DECRBY.spec.ts +++ b/packages/client/lib/commands/DECRBY.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('DECRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 2), - ['DECRBY', 'key', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DECRBY, '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..53a8315130d 100644 --- a/packages/client/lib/commands/DECRBY.ts +++ b/packages/client/lib/commands/DECRBY.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - decrement: number -): RedisCommandArguments { - return ['DECRBY', key, decrement.toString()]; -} - -export declare function transformReply(): number; +export default { + 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 75a29a8f641..3d0364e7523 100644 --- a/packages/client/lib/commands/DEL.spec.ts +++ b/packages/client/lib/commands/DEL.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('DEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['DEL', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(DEL, 'key'), + ['DEL', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['key1', 'key2']), - ['DEL', 'key1', 'key2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(DEL, ['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..da0803f4d1b 100644 --- a/packages/client/lib/commands/DEL.ts +++ b/packages/client/lib/commands/DEL.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 b01f9d650d9..7aa769fc2ee 100644 --- a/packages/client/lib/commands/DISCARD.spec.ts +++ b/packages/client/lib/commands/DISCARD.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './DISCARD'; +import { strict as assert } from 'node:assert'; +import DISCARD from './DISCARD'; +import { parseArgs } from './generic-transformers'; describe('DISCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['DISCARD'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DISCARD), + ['DISCARD'] + ); + }); }); diff --git a/packages/client/lib/commands/DISCARD.ts b/packages/client/lib/commands/DISCARD.ts index acad8a722e1..1d30191c13d 100644 --- a/packages/client/lib/commands/DISCARD.ts +++ b/packages/client/lib/commands/DISCARD.ts @@ -1,7 +1,9 @@ -import { RedisCommandArgument } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['DISCARD']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + 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 aebbf4f3f7c..76fb2ec7c18 100644 --- a/packages/client/lib/commands/DUMP.spec.ts +++ b/packages/client/lib/commands/DUMP.spec.ts @@ -1,11 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import DUMP from './DUMP'; +import { parseArgs } from './generic-transformers'; describe('DUMP', () => { - testUtils.testWithClient('client.dump', async client => { - assert.equal( - await client.dump('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DUMP, 'key'), + ['DUMP', 'key'] + ); + }); + + 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..e442c1cdb2f 100644 --- a/packages/client/lib/commands/DUMP.ts +++ b/packages/client/lib/commands/DUMP.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['DUMP', key]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + IS_READ_ONLY: true, + 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 27f6b2a17d3..38fd1d4270e 100644 --- a/packages/client/lib/commands/ECHO.spec.ts +++ b/packages/client/lib/commands/ECHO.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ECHO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('message'), - ['ECHO', 'message'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ECHO, '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..7935bdc0101 100644 --- a/packages/client/lib/commands/ECHO.ts +++ b/packages/client/lib/commands/ECHO.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(message: RedisCommandArgument): RedisCommandArguments { - return ['ECHO', message]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 7aa029362fd..8ef16eed835 100644 --- a/packages/client/lib/commands/EVAL.spec.ts +++ b/packages/client/lib/commands/EVAL.spec.ts @@ -1,29 +1,26 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(EVAL, '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..cdb8025b0be 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 { CommandParser } from '../client/parser'; +import { RedisArgument, ReplyUnion, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; +export interface EvalOptions { + keys?: Array; + arguments?: Array; +} + +export function parseEvalArguments( + parser: CommandParser, + script: RedisArgument, + options?: EvalOptions +) { + parser.push(script); + if (options?.keys) { + parser.pushKeysLength(options.keys); + } else { + parser.push('0'); + } -export function transformArguments(script: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVAL', script], options); + if (options?.arguments) { + parser.push(...options.arguments) + } } + +export default { + IS_READ_ONLY: false, + 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 08b330ac4f5..c491d6e2308 100644 --- a/packages/client/lib/commands/EVALSHA.spec.ts +++ b/packages/client/lib/commands/EVALSHA.spec.ts @@ -1,14 +1,15 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EVALSHA'; +import { strict as assert } from 'node:assert'; +import EVALSHA from './EVALSHA'; +import { parseArgs } from './generic-transformers'; describe('EVALSHA', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('sha1', { - keys: ['key'], - arguments: ['argument'] - }), - ['EVALSHA', 'sha1', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(EVALSHA, '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..5a9cc771358 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -1,7 +1,11 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { parseEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export function transformArguments(sha1: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVALSHA', sha1], options); -} +export default { + IS_READ_ONLY: false, + 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 939a4a209cb..d3debe933fe 100644 --- a/packages/client/lib/commands/EVALSHA_RO.spec.ts +++ b/packages/client/lib/commands/EVALSHA_RO.spec.ts @@ -1,17 +1,18 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(EVALSHA_RO, '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..24fadb3f486 100644 --- a/packages/client/lib/commands/EVALSHA_RO.ts +++ b/packages/client/lib/commands/EVALSHA_RO.ts @@ -1,9 +1,11 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { parseEvalArguments } 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 { + IS_READ_ONLY: true, + 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 f71d0b2b24a..b5cf1e4e926 100644 --- a/packages/client/lib/commands/EVAL_RO.spec.ts +++ b/packages/client/lib/commands/EVAL_RO.spec.ts @@ -1,31 +1,28 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(EVAL_RO, '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..2438fd9d1dd 100644 --- a/packages/client/lib/commands/EVAL_RO.ts +++ b/packages/client/lib/commands/EVAL_RO.ts @@ -1,9 +1,11 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { parseEvalArguments } 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 { + IS_READ_ONLY: true, + 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 be1a808225e..d2802dd49b3 100644 --- a/packages/client/lib/commands/EXISTS.spec.ts +++ b/packages/client/lib/commands/EXISTS.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('EXISTS', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['EXISTS', 'key'] - ); - }); + describe('parseCommand', () => { + it('string', () => { + assert.deepEqual( + parseArgs(EXISTS, 'key'), + ['EXISTS', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['EXISTS', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(EXISTS, ['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..8ebb28269fe 100644 --- a/packages/client/lib/commands/EXISTS.ts +++ b/packages/client/lib/commands/EXISTS.ts @@ -1,14 +1,13 @@ -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 { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 39f9d70bd93..f3d197b5c69 100644 --- a/packages/client/lib/commands/EXPIRE.spec.ts +++ b/packages/client/lib/commands/EXPIRE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('EXPIRE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 1), - ['EXPIRE', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(EXPIRE, '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( + parseArgs(EXPIRE, '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..1e73b629492 100644 --- a/packages/client/lib/commands/EXPIRE.ts +++ b/packages/client/lib/commands/EXPIRE.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, seconds: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - 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; -} - -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..1949fb051bb 100644 --- a/packages/client/lib/commands/EXPIREAT.spec.ts +++ b/packages/client/lib/commands/EXPIREAT.spec.ts @@ -1,36 +1,40 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('EXPIREAT', () => { - describe('transformArguments', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', 1), - ['EXPIREAT', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + parseArgs(EXPIREAT, 'key', 1), + ['EXPIREAT', 'key', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + parseArgs(EXPIREAT, '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( + parseArgs(EXPIREAT, '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..5bcb94ea321 100644 --- a/packages/client/lib/commands/EXPIREAT.ts +++ b/packages/client/lib/commands/EXPIREAT.ts @@ -1,24 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, timestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - 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; -} - -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..f2c8d3d4521 100644 --- a/packages/client/lib/commands/EXPIRETIME.spec.ts +++ b/packages/client/lib/commands/EXPIRETIME.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 './EXPIRETIME'; +import EXPIRETIME from './EXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('EXPIRETIME', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['EXPIRETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(EXPIRETIME, '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..2bb97fb737b 100644 --- a/packages/client/lib/commands/EXPIRETIME.ts +++ b/packages/client/lib/commands/EXPIRETIME.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['EXPIRETIME', key]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 16094a0dbc3..b23c3516f03 100644 --- a/packages/client/lib/commands/FAILOVER.spec.ts +++ b/packages/client/lib/commands/FAILOVER.spec.ts @@ -1,72 +1,73 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './FAILOVER'; +import { strict as assert } from 'node:assert'; +import FAILOVER from './FAILOVER'; +import { parseArgs } from './generic-transformers'; describe('FAILOVER', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FAILOVER'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(FAILOVER), + ['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( + parseArgs(FAILOVER, { + 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( + parseArgs(FAILOVER, { + 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( + parseArgs(FAILOVER, { + ABORT: true + }), + ['FAILOVER', 'ABORT'] + ); + }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments({ - TIMEOUT: 1 - }), - ['FAILOVER', 'TIMEOUT', '1'] - ); - }); + it('with TIMEOUT', () => { + assert.deepEqual( + parseArgs(FAILOVER, { + 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( + parseArgs(FAILOVER, { + 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..1e98b983f96 100644 --- a/packages/client/lib/commands/FAILOVER.ts +++ b/packages/client/lib/commands/FAILOVER.ts @@ -1,33 +1,35 @@ +import { CommandParser } from '../client/parser'; +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 { - const args = ['FAILOVER']; +export default { + 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'); - } + if (options.TO.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; -} - -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..6c3a65c1448 100644 --- a/packages/client/lib/commands/FCALL.spec.ts +++ b/packages/client/lib/commands/FCALL.spec.ts @@ -1,29 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(FCALL, '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..622060f693c 100644 --- a/packages/client/lib/commands/FCALL.ts +++ b/packages/client/lib/commands/FCALL.ts @@ -1,7 +1,11 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { parseEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export function transformArguments(fn: string, options?: EvalOptions): Array { - return pushEvalArguments(['FCALL', fn], options); -} +export default { + IS_READ_ONLY: false, + 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 18665f92aa6..447e00072be 100644 --- a/packages/client/lib/commands/FCALL_RO.spec.ts +++ b/packages/client/lib/commands/FCALL_RO.spec.ts @@ -1,29 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(FCALL_RO, '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..95effb0e698 100644 --- a/packages/client/lib/commands/FCALL_RO.ts +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -1,9 +1,11 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { parseEvalArguments } 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 { + IS_READ_ONLY: false, + 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 db5bb72e9cb..86daff1973a 100644 --- a/packages/client/lib/commands/FLUSHALL.spec.ts +++ b/packages/client/lib/commands/FLUSHALL.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FLUSHALL', () => { - describe('transformArguments', () => { - it('default', () => { - assert.deepEqual( - transformArguments(), - ['FLUSHALL'] - ); - }); + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + parseArgs(FLUSHALL), + ['FLUSHALL'] + ); + }); - it('ASYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.ASYNC), - ['FLUSHALL', 'ASYNC'] - ); - }); + it('ASYNC', () => { + assert.deepEqual( + parseArgs(FLUSHALL,REDIS_FLUSH_MODES.ASYNC), + ['FLUSHALL', 'ASYNC'] + ); + }); - it('SYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.SYNC), - ['FLUSHALL', 'SYNC'] - ); - }); + it('SYNC', () => { + assert.deepEqual( + parseArgs(FLUSHALL, 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..c39535e8864 100644 --- a/packages/client/lib/commands/FLUSHALL.ts +++ b/packages/client/lib/commands/FLUSHALL.ts @@ -1,16 +1,21 @@ -export enum RedisFlushModes { - ASYNC = 'ASYNC', - SYNC = 'SYNC' -} +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FLUSHALL'); if (mode) { - args.push(mode); + parser.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..795df637cb4 100644 --- a/packages/client/lib/commands/FLUSHDB.spec.ts +++ b/packages/client/lib/commands/FLUSHDB.spec.ts @@ -1,36 +1,37 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FLUSHDB', () => { - describe('transformArguments', () => { - it('default', () => { - assert.deepEqual( - transformArguments(), - ['FLUSHDB'] - ); - }); + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + parseArgs(FLUSHDB), + ['FLUSHDB'] + ); + }); - it('ASYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.ASYNC), - ['FLUSHDB', 'ASYNC'] - ); - }); + it('ASYNC', () => { + assert.deepEqual( + parseArgs(FLUSHDB, REDIS_FLUSH_MODES.ASYNC), + ['FLUSHDB', 'ASYNC'] + ); + }); - it('SYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.SYNC), - ['FLUSHDB', 'SYNC'] - ); - }); + it('SYNC', () => { + assert.deepEqual( + parseArgs(FLUSHDB, 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..5639f69a611 100644 --- a/packages/client/lib/commands/FLUSHDB.ts +++ b/packages/client/lib/commands/FLUSHDB.ts @@ -1,13 +1,15 @@ -import { RedisFlushModes } from './FLUSHALL'; - -export function transformArguments(mode?: RedisFlushModes): Array { - const args = ['FLUSHDB']; - +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { RedisFlushMode } from './FLUSHALL'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FLUSHDB'); if (mode) { - args.push(mode); + parser.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..b33ea25916b 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts @@ -1,24 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION DELETE', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('library'), - ['FUNCTION', 'DELETE', 'library'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(FUNCTION_DELETE, '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..dbfb044928e 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -1,7 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(library: string): RedisCommandArguments { - return ['FUNCTION', 'DELETE', library]; -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + 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 360ec6b7453..bbd6302bb6a 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION DUMP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'DUMP'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(FUNCTION_DUMP), + ['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..2d0dbdd4455 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['FUNCTION', 'DUMP']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 12009d03363..4fe90bdb607 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION FLUSH', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'FLUSH'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(FUNCTION_FLUSH), + ['FUNCTION', 'FLUSH'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('SYNC'), - ['FUNCTION', 'FLUSH', 'SYNC'] - ); - }); + it('with mode', () => { + assert.deepEqual( + parseArgs(FUNCTION_FLUSH, '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..4ca59e4464e 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -1,13 +1,16 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { RedisFlushMode } from './FLUSHALL'; -export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments { - const args = ['FUNCTION', 'FLUSH']; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FUNCTION', 'FLUSH'); if (mode) { - args.push(mode); + parser.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..c4dbd124d30 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.spec.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.spec.ts @@ -1,14 +1,15 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION KILL', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'KILL'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(FUNCTION_KILL), + ['FUNCTION', 'KILL'] + ); + }); }); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts index 517272e8376..8b5351e93ab 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -1,7 +1,11 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['FUNCTION', 'KILL']; -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 80723d070de..6d9b28acf90 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.spec.ts @@ -1,41 +1,46 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION LIST', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'LIST'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(FUNCTION_LIST), + ['FUNCTION', 'LIST'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['FUNCTION', 'LIST', 'patter*'] - ); - }); + it('with LIBRARYNAME', () => { + assert.deepEqual( + parseArgs(FUNCTION_LIST, { + 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..82e3697eadc 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -1,16 +1,50 @@ -import { RedisCommandArguments } from '.'; -import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NullReply, SetReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export function transformArguments(pattern?: string): RedisCommandArguments { - const args = ['FUNCTION', 'LIST']; +export interface FunctionListOptions { + LIBRARYNAME?: RedisArgument; +} - if (pattern) { - args.push(pattern); - } +export type FunctionListReplyItem = [ + [BlobStringReply<'library_name'>, BlobStringReply | NullReply], + [BlobStringReply<'engine'>, BlobStringReply], + [BlobStringReply<'functions'>, ArrayReply, BlobStringReply], + [BlobStringReply<'description'>, BlobStringReply | NullReply], + [BlobStringReply<'flags'>, SetReply], + ]>>] +]; - return args; -} +export type FunctionListReply = ArrayReply>; -export function transformReply(reply: Array): Array { - return reply.map(transformFunctionListItemReply); -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, options?: FunctionListOptions) { + parser.push('FUNCTION', 'LIST'); + + if (options?.LIBRARYNAME) { + parser.push('LIBRARYNAME', options.LIBRARYNAME); + } + }, + 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..f44db9ba037 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.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 { 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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(FUNCTION_LIST_WITHCODE), + ['FUNCTION', 'LIST', 'WITHCODE'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['FUNCTION', 'LIST', 'patter*', 'WITHCODE'] - ); - }); + it('with LIBRARYNAME', () => { + assert.deepEqual( + parseArgs(FUNCTION_LIST_WITHCODE, { + 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..208bc5fd303 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts @@ -1,26 +1,37 @@ -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 { + NOT_KEYED_COMMAND: FUNCTION_LIST.NOT_KEYED_COMMAND, + IS_READ_ONLY: FUNCTION_LIST.IS_READ_ONLY, + parseCommand(...args: Parameters) { + FUNCTION_LIST.parseCommand(...args); + args[0].push('WITHCODE'); + }, + 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..c0a511bffc9 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts @@ -1,36 +1,79 @@ -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'; +import { parseArgs } from './generic-transformers'; +import { CommandParser } from '../client/parser'; + + + +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 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( + parseArgs(FUNCTION_LOAD, 'code'), + ['FUNCTION', 'LOAD', 'code'] + ); + }); + + it('with REPLACE', () => { + assert.deepEqual( + parseArgs(FUNCTION_LOAD, '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..40b8ea8c0f4 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -1,22 +1,21 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, 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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + 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; -} - -export declare function transformReply(): string; + 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 a5c2e2dcc72..72d7d1d6204 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts @@ -1,37 +1,41 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(FUNCTION_RESTORE, 'dump'), + ['FUNCTION', 'RESTORE', 'dump'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('dump', 'APPEND'), - ['FUNCTION', 'RESTORE', 'dump', 'APPEND'] - ); - }); + it('with mode', () => { + assert.deepEqual( + parseArgs(FUNCTION_RESTORE, '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..944813f25e5 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -1,16 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '../RESP/types'; -export function transformArguments( - dump: RedisCommandArgument, - mode?: 'FLUSH' | 'APPEND' | 'REPLACE' -): RedisCommandArguments { - const args = ['FUNCTION', 'RESTORE', dump]; - - if (mode) { - args.push(mode); - } - - return args; +export interface FunctionRestoreOptions { + mode?: 'FLUSH' | 'APPEND' | 'REPLACE'; } -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, dump: RedisArgument, options?: FunctionRestoreOptions) { + parser.push('FUNCTION', 'RESTORE', dump); + + if (options?.mode) { + parser.push(options.mode); + } + }, + 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..a3c5e00fe72 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.spec.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.spec.ts @@ -1,25 +1,26 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION STATS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(FUNCTION_STATS), + ['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..908be5476e0 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -1,56 +1,71 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { Command, TuplesToMapReply, BlobStringReply, NullReply, NumberReply, MapReply, Resp2Reply, UnwrapReply } from '../RESP/types'; +import { isNullReply } from './generic-transformers'; -export function transformArguments(): RedisCommandArguments { - return ['FUNCTION', 'STATS']; -} +type RunningScript = NullReply | TuplesToMapReply<[ + [BlobStringReply<'name'>, BlobStringReply], + [BlobStringReply<'command'>, BlobStringReply], + [BlobStringReply<'duration_ms'>, NumberReply] +]>; -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; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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; + +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..d947141a318 100644 --- a/packages/client/lib/commands/GEOADD.spec.ts +++ b/packages/client/lib/commands/GEOADD.spec.ts @@ -1,95 +1,101 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOADD, '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( + parseArgs(GEOADD, '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( + parseArgs(GEOADD, '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( + parseArgs(GEOADD, '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( + parseArgs(GEOADD, '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( + parseArgs(GEOADD, '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..31bf457e158 100644 --- a/packages/client/lib/commands/GEOADD.ts +++ b/packages/client/lib/commands/GEOADD.ts @@ -1,53 +1,66 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoCoordinates } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: GeoMember | Array, options?: GeoAddOptions -): RedisCommandArguments { - const args = ['GEOADD', key]; + ) { + parser.push('GEOADD') + parser.pushKey(key); - if ((options as NX)?.NX) { - args.push('NX'); - } else if ((options as XX)?.XX) { - args.push('XX'); + if (options?.condition) { + parser.push(options.condition); + } else if (options?.NX) { + parser.push('NX'); + } else if (options?.XX) { + parser.push('XX'); } if (options?.CH) { - args.push('CH'); + parser.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(parser, member); + } + } else { + pushMember(parser, toAdd); } - return args; -} + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; -export declare function transformReply(): number; +function pushMember( + parser: CommandParser, + { longitude, latitude, member }: GeoMember +) { + 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 bbc62480ee1..a23df405d1d 100644 --- a/packages/client/lib/commands/GEODIST.spec.ts +++ b/packages/client/lib/commands/GEODIST.spec.ts @@ -1,57 +1,55 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEODIST, '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( + parseArgs(GEODIST, '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..ba4d3080a71 100644 --- a/packages/client/lib/commands/GEODIST.ts +++ b/packages/client/lib/commands/GEODIST.ts @@ -1,25 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoUnits } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, + key: RedisArgument, + member1: RedisArgument, + member2: RedisArgument, unit?: GeoUnits -): RedisCommandArguments { - 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; -} - -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..ad26dff8434 100644 --- a/packages/client/lib/commands/GEOHASH.spec.ts +++ b/packages/client/lib/commands/GEOHASH.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOHASH, '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( + parseArgs(GEOHASH, '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..c3265d13157 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 { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 9c08ccd08f5..002d16d0256 100644 --- a/packages/client/lib/commands/GEOPOS.spec.ts +++ b/packages/client/lib/commands/GEOPOS.spec.ts @@ -1,73 +1,166 @@ -import { strict as assert } from 'assert'; +import { strict as assert, fail } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './GEOPOS'; +import GEOPOS from './GEOPOS'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOPOS, '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( + parseArgs(GEOPOS, '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); + 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/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts index 0a5f079deeb..6bdbb65ac46 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 { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '../RESP/types'; +import { 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(['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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { + parser.push('GEOPOS'); + parser.pushKey(key); + parser.pushVariadic(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..3c33395c5f6 100644 --- a/packages/client/lib/commands/GEORADIUS.spec.ts +++ b/packages/client/lib/commands/GEORADIUS.spec.ts @@ -1,35 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - ['GEORADIUS', 'key', '1', '2', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GEORADIUS, '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..5e8d880ab5e 100644 --- a/packages/client/lib/commands/GEORADIUS.ts +++ b/packages/client/lib/commands/GEORADIUS.ts @@ -1,25 +1,27 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, GeoCoordinates, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { GeoCoordinates, GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; +export function parseGeoRadiusArguments( + parser: CommandParser, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + options?: GeoSearchOptions +) { + parser.pushKey(key); + parser.push(from.longitude.toString(), from.latitude.toString(), radius.toString(), unit); -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUS'], - key, - coordinates, - radius, - unit, - options - ); + parseGeoSearchOptions(parser, options) } -export declare function transformReply(): Array; +export default { + IS_READ_ONLY: false, + 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 8cc4212c839..c81c3d75815 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm'), - ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GEORADIUSBYMEMBER, '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..be4ca54650c 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts @@ -1,25 +1,33 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; +export function parseGeoRadiusByMemberArguments( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + options?: GeoSearchOptions +) { + parser.pushKey(key); + parser.push(from, radius.toString(), unit); -export const IS_READ_ONLY = true; + parseGeoSearchOptions(parser, options); +} -export function transformArguments( - key: RedisCommandArgument, - member: string, +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, radius: number, unit: GeoUnits, options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUSBYMEMBER'], - key, - member, - radius, - unit, - options - ); -} - -export declare function transformReply(): Array; + ) { + 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/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..bd4aa86dec1 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER_RO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm'), - ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GEORADIUSBYMEMBER_RO, '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..335eea08133 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts @@ -1,25 +1,13 @@ -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, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 7904a763998..52b31b03594 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts @@ -1,31 +1,45 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEORADIUSBYMEMBER_RO_WITH, '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..06835438016 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts @@ -1,30 +1,13 @@ -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, { parseGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 new file mode 100644 index 00000000000..9edb08d1eae --- /dev/null +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts @@ -0,0 +1,40 @@ +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( + parseArgs(GEORADIUSBYMEMBER_STORE, 'key', 'member', 3, 'm', 'destination'), + ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STORE', 'destination'] + ); + }); + + it('STOREDIST', () => { + assert.deepEqual( + parseArgs(GEORADIUSBYMEMBER_STORE, '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..676df34dd5a --- /dev/null +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts @@ -0,0 +1,33 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; +import { GeoSearchOptions, GeoUnits } from './GEOSEARCH'; + +export interface GeoRadiusStoreOptions extends GeoSearchOptions { + STOREDIST?: boolean; +} + +export default { + IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + destination: RedisArgument, + options?: GeoRadiusStoreOptions + ) { + parser.push('GEORADIUSBYMEMBER') + parseGeoRadiusByMemberArguments(parser, key, from, radius, unit, options); + + if (options?.STOREDIST) { + parser.push('STOREDIST'); + parser.pushKey(destination); + } else { + parser.push('STORE'); + parser.pushKey(destination); + } + }, + 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..9d634d60656 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts @@ -1,31 +1,45 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEORADIUSBYMEMBER_WITH, '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..eefae0b27a9 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts @@ -1,30 +1,39 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoUnits } from './generic-transformers'; -import { transformArguments as transformGeoRadiusArguments } from './GEORADIUSBYMEMBER'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; +import { GeoSearchOptions, GeoUnits, parseGeoSearchOptions } from './GEOSEARCH'; +import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER'; +export function parseGeoRadiusByMemberWithArguments( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions +) { + parser.pushKey(key); + parser.push(from, radius.toString(), unit); + parseGeoSearchOptions(parser, options); -export function transformArguments( - key: RedisCommandArgument, - member: string, + parser.push(...replyWith); + parser.preserve = replyWith; +} + +export default { + IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, radius: number, unit: GeoUnits, replyWith: Array, options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = transformGeoRadiusArguments( - key, - member, - radius, - unit, - options - ); - - args.push(...replyWith); - - args.preserve = replyWith; - - return args; -} - -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; + ) { + 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/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..917eba3ab8e 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.spec.ts @@ -1,35 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEORADIUS_RO, '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..5db65d9dc9b 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.ts @@ -1,25 +1,12 @@ -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, { parseGeoRadiusArguments } from './GEORADIUS'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 21b00ff90b8..01d79954b64 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts @@ -1,40 +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 './GEORADIUS_RO_WITH'; +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', () => { - 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( + parseArgs(GEORADIUS_RO_WITH, '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..cee1679382b 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts @@ -1,30 +1,13 @@ -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 { parseGeoRadiusWithArguments } from './GEORADIUS_WITH'; +import GEORADIUS_WITH from './GEORADIUS_WITH'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 new file mode 100644 index 00000000000..9a9bcf37bcf --- /dev/null +++ b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts @@ -0,0 +1,49 @@ +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( + parseArgs(GEORADIUS_STORE, 'key', { + longitude: 1, + latitude: 2 + }, 3, 'm', 'destination'), + ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STORE', 'destination'] + ); + }); + + it('STOREDIST', () => { + assert.deepEqual( + parseArgs(GEORADIUS_STORE, '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..18459d44217 --- /dev/null +++ b/packages/client/lib/commands/GEORADIUS_STORE.ts @@ -0,0 +1,32 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; +import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; + +export interface GeoRadiusStoreOptions extends GeoSearchOptions { + STOREDIST?: boolean; +} + +export default { + IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + destination: RedisArgument, + options?: GeoRadiusStoreOptions + ) { + parser.push('GEORADIUS'); + parseGeoRadiusArguments(parser, key, from, radius, unit, options); + if (options?.STOREDIST) { + parser.push('STOREDIST'); + parser.pushKey(destination); + } else { + parser.push('STORE'); + parser.pushKey(destination); + } + }, + 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..f514c9be96f 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts @@ -1,40 +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 './GEORADIUS_WITH'; +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', () => { - 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( + parseArgs(GEORADIUS_WITH, '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..ac4c8b7bb1b 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.ts @@ -1,30 +1,36 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoCoordinates, GeoUnits } from './generic-transformers'; -import { transformArguments as 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 { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS'; +export function parseGeoRadiusWithArguments( + parser: CommandParser, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions, +) { + parseGeoRadiusArguments(parser, key, from, radius, unit, options) + parser.pushVariadic(replyWith); + parser.preserve = replyWith; +} -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, +export default { + IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: 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 { transformGeoMembersWithReply as transformReply } from './generic-transformers'; + ) { + 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 ec0d4bcc4f8..4cd7e61a0ac 100644 --- a/packages/client/lib/commands/GEOSEARCH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH.spec.ts @@ -1,37 +1,88 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOSEARCH, 'key', 'member', { + radius: 1, + unit: 'm' + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] + ); + }); + + it('FROMLONLAT, BYBOX, without options', () => { + assert.deepEqual( + parseArgs(GEOSEARCH, '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( + parseArgs(GEOSEARCH, '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' - }), - [] + parseArgs(GEOSEARCH, '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' - }), - [] + parseArgs(GEOSEARCH, '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..8c77fd89239 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -1,17 +1,99 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, 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 parseGeoSearchArguments( + parser: CommandParser, + key: RedisArgument, + from: GeoSearchFrom, + by: GeoSearchBy, + options?: GeoSearchOptions, + store?: RedisArgument +) { + if (store !== undefined) { + parser.pushKey(store); + } + + parser.pushKey(key); + + if (typeof from === 'string' || from instanceof Buffer) { + parser.push('FROMMEMBER', from); + } else { + parser.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString()); + } + + if ('radius' in by) { + parser.push('BYRADIUS', by.radius.toString(), by.unit); + } else { + parser.push('BYBOX', by.width.toString(), by.height.toString(), by.unit); + } -export function transformArguments( - key: RedisCommandArgument, + parseGeoSearchOptions(parser, options); +} + +export type GeoCountArgument = number | { + value: number; + ANY?: boolean; +}; + +export interface GeoSearchOptions { + SORT?: 'ASC' | 'DESC'; + COUNT?: GeoCountArgument; +} + +export function parseGeoSearchOptions( + parser: CommandParser, + options?: GeoSearchOptions +) { + if (options?.SORT) { + parser.push(options.SORT); + } + + if (options?.COUNT) { + if (typeof options.COUNT === 'number') { + parser.push('COUNT', options.COUNT.toString()); + } else { + parser.push('COUNT', options.COUNT.value.toString()); + + if (options.COUNT.ANY) { + parser.push('ANY'); + } + } + } +} + +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options); -} - -export declare function transformReply(): Array; + ) { + 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 eb32fa134e4..b8427ae0412 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts @@ -1,81 +1,45 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOSEARCHSTORE, '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( + parseArgs(GEOSEARCHSTORE, '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..eb8e12abe6d 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 { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, parseGeoSearchArguments } 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + destination: RedisArgument, + source: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchStoreOptions -): RedisCommandArguments { - 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; -} - -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..973e5d5827f 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts @@ -1,42 +1,50 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GEOSEARCH_WITH, '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..65e3975b72f 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 { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, replyWith: Array, options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = geoSearchTransformArguments(key, from, by, options); + ) { + GEOSEARCH.parseCommand(parser, key, from, by, options); + parser.push(...replyWith); + parser.preserve = replyWith; + }, + 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; - args.push(...replyWith); + const item: GeoReplyWithMember = { + member: unwrapped[0] + }; - args.preserve = replyWith; - - return args; -} + 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..3e630d03e0b 100644 --- a/packages/client/lib/commands/GET.spec.ts +++ b/packages/client/lib/commands/GET.spec.ts @@ -1,33 +1,23 @@ -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 { parseArgs } from './generic-transformers'; +import GET from './GET'; describe('GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GET, '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..ca013752ae5 100644 --- a/packages/client/lib/commands/GET.ts +++ b/packages/client/lib/commands/GET.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['GET', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 4206084eced..66d2798313c 100644 --- a/packages/client/lib/commands/GETBIT.spec.ts +++ b/packages/client/lib/commands/GETBIT.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GETBIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['GETBIT', 'key', '0'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(GETBIT, '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..023ba0fb607 100644 --- a/packages/client/lib/commands/GETBIT.ts +++ b/packages/client/lib/commands/GETBIT.ts @@ -1,15 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - return ['GETBIT', key, offset.toString()]; -} - -export declare function transformReply(): BitValue; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 db3a486696a..15ad5918008 100644 --- a/packages/client/lib/commands/GETDEL.spec.ts +++ b/packages/client/lib/commands/GETDEL.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GETDEL', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GETDEL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GETDEL, '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..a39014109f1 100644 --- a/packages/client/lib/commands/GETDEL.ts +++ b/packages/client/lib/commands/GETDEL.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['GETDEL', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + IS_READ_ONLY: true, + 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 1bf86089da1..5965d8f196f 100644 --- a/packages/client/lib/commands/GETEX.spec.ts +++ b/packages/client/lib/commands/GETEX.spec.ts @@ -1,96 +1,123 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, '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( + parseArgs(GETEX, 'key', { + PXAT: d + }), + ['GETEX', 'key', 'PXAT', d.getTime().toString()] ); - }, GLOBAL.CLUSTERS.OPEN); + }); + }); + + it('PERSIST (backwards compatibility)', () => { + assert.deepEqual( + parseArgs(GETEX, '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..e5ae0b691a7 100644 --- a/packages/client/lib/commands/GETEX.ts +++ b/packages/client/lib/commands/GETEX.ts @@ -1,39 +1,77 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - const args = ['GETEX', key]; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options: GetExOptions) { + parser.push('GETEX'); + parser.pushKey(key); - if ('EX' in mode) { - args.push('EX', mode.EX.toString()); - } else if ('PX' in mode) { - args.push('PX', mode.PX.toString()); - } else if ('EXAT' in mode) { - args.push('EXAT', transformEXAT(mode.EXAT)); - } else if ('PXAT' in mode) { - args.push('PXAT', transformPXAT(mode.PXAT)); - } else { // PERSIST - args.push('PERSIST'); - } - - return args; -} + if ('type' in options) { + switch (options.type) { + case 'EX': + case 'PX': + parser.push(options.type, options.value.toString()); + break; + + case 'EXAT': + case 'PXAT': + parser.push(options.type, transformEXAT(options.value)); + break; -export declare function transformReply(): RedisCommandArgument | null; + case 'PERSIST': + parser.push('PERSIST'); + break; + } + } else { + if ('EX' in options) { + parser.push('EX', options.EX.toString()); + } else if ('PX' in options) { + parser.push('PX', options.PX.toString()); + } else if ('EXAT' in options) { + parser.push('EXAT', transformEXAT(options.EXAT)); + } else if ('PXAT' in options) { + parser.push('PXAT', transformPXAT(options.PXAT)); + } else { // PERSIST + parser.push('PERSIST'); + } + } + }, + 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..8a8e7dde038 100644 --- a/packages/client/lib/commands/GETRANGE.spec.ts +++ b/packages/client/lib/commands/GETRANGE.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GETRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, -1), - ['GETRANGE', 'key', '0', '-1'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(GETRANGE, '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..ce0db6e3c03 100644 --- a/packages/client/lib/commands/GETRANGE.ts +++ b/packages/client/lib/commands/GETRANGE.ts @@ -1,15 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - start: number, - end: number -): RedisCommandArguments { - return ['GETRANGE', key, start.toString(), end.toString()]; -} - -export declare function transformReply(): RedisCommandArgument; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 73fbcec57ea..5b162c16cc4 100644 --- a/packages/client/lib/commands/GETSET.spec.ts +++ b/packages/client/lib/commands/GETSET.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('GETSET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['GETSET', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(GETSET, '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..1b3312548e4 100644 --- a/packages/client/lib/commands/GETSET.ts +++ b/packages/client/lib/commands/GETSET.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { - return ['GETSET', key, value]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + IS_READ_ONLY: true, + 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 eb24bcfacbd..767d916e147 100644 --- a/packages/client/lib/commands/HDEL.spec.ts +++ b/packages/client/lib/commands/HDEL.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HDEL', 'key', 'field'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(HDEL, 'key', 'field'), + ['HDEL', 'key', 'field'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['HDEL', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(HDEL, '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..713d19a9b2a 100644 --- a/packages/client/lib/commands/HDEL.ts +++ b/packages/client/lib/commands/HDEL.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 12d6d98c7c9..5d11be344c1 100644 --- a/packages/client/lib/commands/HELLO.spec.ts +++ b/packages/client/lib/commands/HELLO.spec.ts @@ -1,76 +1,72 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(HELLO), + ['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( + parseArgs(HELLO, 3), + ['HELLO', '3'] + ); + }); - it('with protover, clientName', () => { - assert.deepEqual( - transformArguments({ - protover: 3, - clientName: 'clientName' - }), - ['HELLO', '3', 'SETNAME', 'clientName'] - ); - }); + it('with protover, AUTH', () => { + assert.deepEqual( + parseArgs(HELLO, 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( + parseArgs(HELLO, 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( + parseArgs(HELLO, 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..5d25998f987 100644 --- a/packages/client/lib/commands/HELLO.ts +++ b/packages/client/lib/commands/HELLO.ts @@ -1,65 +1,58 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { AuthOptions } from './AUTH'; - -interface HelloOptions { - protover: number; - auth?: Required; - clientName?: string; +import { CommandParser } from '../client/parser'; +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 { + parseCommand(parser: CommandParser, protover?: RespVersions, options?: HelloOptions) { + parser.push('HELLO'); + + if (protover) { + parser.push(protover.toString()); + + if (options?.AUTH) { + parser.push( + 'AUTH', + options.AUTH.username, + options.AUTH.password + ); + } + + if (options?.SETNAME) { + parser.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..acd462ab7e2 100644 --- a/packages/client/lib/commands/HEXISTS.spec.ts +++ b/packages/client/lib/commands/HEXISTS.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HEXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HEXISTS', 'key', 'field'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(HEXISTS, '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..9bb517b7df4 100644 --- a/packages/client/lib/commands/HEXISTS.ts +++ b/packages/client/lib/commands/HEXISTS.ts @@ -1,12 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { - return ['HEXISTS', key, field]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 3714f617f58..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 { transformArguments } from './HEXPIRE'; +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( - transformArguments('key', 'field', 1), + parseArgs(HEXPIRE, 'key', 'field', 1), ['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HEXPIRE, 'key', ['field1', 'field2'], 1), ['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - 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 938f9039939..55e2f5a9be1 100644 --- a/packages/client/lib/commands/HEXPIRE.ts +++ b/packages/client/lib/commands/HEXPIRE.ts @@ -1,44 +1,39 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } 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 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); - } +export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION]; - args.push('FIELDS'); +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + seconds: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT' + ) { + parser.push('HEXPIRE'); + parser.pushKey(key); + parser.push(seconds.toString()); + + if (mode) { + parser.push(mode); + } - return pushVerdictArgument(args, fields); -} + parser.push('FIELDS'); -export declare function transformReply(): Array; \ No newline at end of file + parser.pushVariadicWithLength(fields); + }, + 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 1c65fb61773..c7cc9fe749b 100644 --- a/packages/client/lib/commands/HEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HEXPIREAT.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HEXPIREAT', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + parseArgs(HEXPIREAT, 'key', 'field', 1), ['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - 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( - 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( - 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 58c52d3a1f6..1370f2ecd65 100644 --- a/packages/client/lib/commands/HEXPIREAT.ts +++ b/packages/client/lib/commands/HEXPIREAT.ts @@ -1,28 +1,26 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument, 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 const FIRST_KEY_INDEX = 1; +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' + ) { + parser.push('HEXPIREAT'); + parser.pushKey(key); + parser.push(transformEXAT(timestamp)); -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array, - timestamp: number | Date, - mode?: 'NX' | 'XX' | 'GT' | 'LT' -) { - const args = [ - 'HEXPIREAT', - key, - transformEXAT(timestamp) - ]; + if (mode) { + parser.push(mode); + } - if (mode) { - args.push(mode); - } + parser.push('FIELDS') - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array; \ No newline at end of file + parser.pushVariadicWithLength(fields); + }, + 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 9c3eb024bed..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 { HASH_EXPIRATION_TIME, transformArguments } from './HEXPIRETIME'; +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( - transformArguments('key', 'field'), + parseArgs(HEXPIRETIME, 'key', 'field'), ['HEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - 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 01764b1032d..697d327db16 100644 --- a/packages/client/lib/commands/HEXPIRETIME.ts +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -1,21 +1,25 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types'; +import { 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 { + IS_READ_ONLY: true, + 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 6b6d0a3ee22..47061876aea 100644 --- a/packages/client/lib/commands/HGET.spec.ts +++ b/packages/client/lib/commands/HGET.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HGET', 'key', 'field'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HGET, '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..fcd9334eb0a 100644 --- a/packages/client/lib/commands/HGET.ts +++ b/packages/client/lib/commands/HGET.ts @@ -1,14 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { - return ['HGET', key, field]; -} - -export declare function transformReply(): RedisCommandArgument | undefined; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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.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..a2c3011c4c2 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - return ['HGETALL', key]; -} - -export { transformTuplesReply as transformReply } from './generic-transformers'; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HGETALL'); + parser.pushKey(key); + }, + TRANSFORM_LEGACY_REPLY: true, + transformReply: { + 2: transformTuplesReply, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; 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/HINCRBY.spec.ts b/packages/client/lib/commands/HINCRBY.spec.ts index de406217921..ad382d97a99 100644 --- a/packages/client/lib/commands/HINCRBY.spec.ts +++ b/packages/client/lib/commands/HINCRBY.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 1), - ['HINCRBY', 'key', 'field', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HINCRBY, '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..3638e408f7d 100644 --- a/packages/client/lib/commands/HINCRBY.ts +++ b/packages/client/lib/commands/HINCRBY.ts @@ -1,13 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + field: RedisArgument, increment: number -): RedisCommandArguments { - return ['HINCRBY', key, field, increment.toString()]; -} - -export declare function transformReply(): number; + ) { + 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 bd0147a3481..2edbd6f9477 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HINCRBYFLOAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 1.5), - ['HINCRBYFLOAT', 'key', 'field', '1.5'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HINCRBYFLOAT, '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..6d527583c71 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.ts @@ -1,13 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + field: RedisArgument, increment: number -): RedisCommandArguments { - return ['HINCRBYFLOAT', key, field, increment.toString()]; -} - -export declare function transformReply(): number; + ) { + 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 f94538f67d3..58445696d20 100644 --- a/packages/client/lib/commands/HKEYS.spec.ts +++ b/packages/client/lib/commands/HKEYS.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HKEYS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HKEYS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HKEYS, '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..f07a1ac127f 100644 --- a/packages/client/lib/commands/HKEYS.ts +++ b/packages/client/lib/commands/HKEYS.ts @@ -1,9 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['HKEYS', key]; -} - -export declare function transformReply(): Array; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 be9d4b13a7d..640e461ad07 100644 --- a/packages/client/lib/commands/HLEN.spec.ts +++ b/packages/client/lib/commands/HLEN.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HLEN, '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..e3b89da3e7d 100644 --- a/packages/client/lib/commands/HLEN.ts +++ b/packages/client/lib/commands/HLEN.ts @@ -1,9 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['HLEN', key]; -} - -export declare function transformReply(): number; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 a7c934b760d..8cc90e4abd5 100644 --- a/packages/client/lib/commands/HMGET.spec.ts +++ b/packages/client/lib/commands/HMGET.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HMGET', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HMGET', 'key', 'field'] - ); - }); + describe('parseCommand', () => { + it('string', () => { + assert.deepEqual( + parseArgs(HMGET, 'key', 'field'), + ['HMGET', 'key', 'field'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['field1', 'field2']), - ['HMGET', 'key', 'field1', 'field2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(HMGET, '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..51ba937339f 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 { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 8cf3f1fe221..0b317977cbf 100644 --- a/packages/client/lib/commands/HPERSIST.spec.ts +++ b/packages/client/lib/commands/HPERSIST.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HPERSIST', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPERSIST', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + parseArgs(HPERSIST, 'key', 'field'), ['HPERSIST', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - 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 862a7548ac1..fd0f320e65a 100644 --- a/packages/client/lib/commands/HPERSIST.ts +++ b/packages/client/lib/commands/HPERSIST.ts @@ -1,10 +1,17 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { 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 { + 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 852d9f5bd21..2f68fb9b7f3 100644 --- a/packages/client/lib/commands/HPEXPIRE.spec.ts +++ b/packages/client/lib/commands/HPEXPIRE.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HEXPIRE', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,21 +10,21 @@ describe('HEXPIRE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + parseArgs(HPEXPIRE, 'key', 'field', 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HPEXPIRE, 'key', ['field1', 'field2'], 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - 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 afbb056ed4e..34513c34e3b 100644 --- a/packages/client/lib/commands/HPEXPIRE.ts +++ b/packages/client/lib/commands/HPEXPIRE.ts @@ -1,24 +1,27 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; -import { HashExpiration } from "./HEXPIRE"; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; +import { HashExpiration } from './HEXPIRE'; -export const FIRST_KEY_INDEX = 1; +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + ms: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT', + ) { + parser.push('HPEXPIRE'); + parser.pushKey(key); + parser.push(ms.toString()); -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array, - ms: number, - mode?: 'NX' | 'XX' | 'GT' | 'LT', -) { - const args = ['HPEXPIRE', key, ms.toString()]; + if (mode) { + parser.push(mode); + } - if (mode) { - args.push(mode); - } + parser.push('FIELDS') - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file + 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 9747cca1a2d..7c369980bf4 100644 --- a/packages/client/lib/commands/HPEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HPEXPIREAT', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + parseArgs(HPEXPIREAT, 'key', 'field', 1), ['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - 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( - transformArguments('key', ['field1'], d), + parseArgs(HPEXPIREAT, 'key', ['field1'], d), ['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1'] ); }); it('with set option', () => { assert.deepEqual( - 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 b6e01d8ee5c..14288d7ae90 100644 --- a/packages/client/lib/commands/HPEXPIREAT.ts +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -1,25 +1,28 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument, transformEXAT, transformPXAT } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument, transformPXAT } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; -export const FIRST_KEY_INDEX = 1; -export const IS_READ_ONLY = true; +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' + ) { + parser.push('HPEXPIREAT'); + parser.pushKey(key); + parser.push(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) { + parser.push(mode); + } - if (mode) { - args.push(mode); - } + parser.push('FIELDS') - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file + 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 ff03b73c71d..5673a725afc 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HPEXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPEXPIRETIME', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + parseArgs(HPEXPIRETIME, 'key', 'field'), ['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - 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 22a794ccefa..cacce25a85f 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -1,11 +1,18 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { 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 { + IS_READ_ONLY: true, + 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 ddca26ea85b..baaa11b19c8 100644 --- a/packages/client/lib/commands/HPTTL.spec.ts +++ b/packages/client/lib/commands/HPTTL.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HPTTL', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + parseArgs(HPTTL, 'key', 'field'), ['HPTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - 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 988b805c0c9..b9cd54a850d 100644 --- a/packages/client/lib/commands/HPTTL.ts +++ b/packages/client/lib/commands/HPTTL.ts @@ -1,11 +1,18 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { 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 { + IS_READ_ONLY: true, + 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 df0a4fc7a1d..151636057a0 100644 --- a/packages/client/lib/commands/HRANDFIELD.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD.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 './HRANDFIELD'; +import HRANDFIELD from './HRANDFIELD'; +import { parseArgs } from './generic-transformers'; describe('HRANDFIELD', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HRANDFIELD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HRANDFIELD, '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..3383b94dcb2 100644 --- a/packages/client/lib/commands/HRANDFIELD.ts +++ b/packages/client/lib/commands/HRANDFIELD.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['HRANDFIELD', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 4bfce0f7e23..ee3fc984d55 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.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 './HRANDFIELD_COUNT'; +import HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(HRANDFIELD_COUNT, '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..62abe97e350 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts @@ -1,16 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: true, + 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.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..aa8ebad1b93 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -1,16 +1,41 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformHRandFieldCountArguments } from './HRANDFIELD_COUNT'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('HRANDFIELD'); + parser.pushKey(key); + parser.push(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..9e489f6190d 100644 --- a/packages/client/lib/commands/HSCAN.spec.ts +++ b/packages/client/lib/commands/HSCAN.spec.ts @@ -1,90 +1,83 @@ -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 { parseArgs } from './generic-transformers'; +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( + parseArgs(HSCAN, '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( + parseArgs(HSCAN, '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( + parseArgs(HSCAN, '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( + parseArgs(HSCAN, '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..e1e40663a07 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -1,44 +1,37 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, parseScanArguments } 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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + parser.push('HSCAN'); + parser.pushKey(key); + parseScanArguments(parser, 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..83a452a6897 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 { parseArgs } from './generic-transformers'; 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( + parseArgs(HSCAN_NOVALUES, 'key', '0'), + ['HSCAN', 'key', '0', 'NOVALUES'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + parseArgs(HSCAN_NOVALUES, '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( + parseArgs(HSCAN_NOVALUES, '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( + parseArgs(HSCAN_NOVALUES, '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..eff61a7aab0 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -1,27 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions } from './generic-transformers'; -import { HScanRawReply, transformArguments as transformHScanArguments } from './HSCAN'; +import { BlobStringReply, Command } from '../RESP/types'; +import HSCAN from './HSCAN'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HSCAN'; +export default { + IS_READ_ONLY: true, + parseCommand(...args: Parameters) { + const parser = args[0]; -export function transformArguments( - key: RedisCommandArgument, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - const args = transformHScanArguments(key, cursor, options); - args.push('NOVALUES'); - return args; -} - -interface HScanNoValuesReply { - cursor: number; - keys: Array; -} - -export function transformReply([cursor, rawData]: HScanRawReply): HScanNoValuesReply { + HSCAN.parseCommand(...args); + parser.push('NOVALUES'); + }, + 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..2cb53e6485a 100644 --- a/packages/client/lib/commands/HSET.spec.ts +++ b/packages/client/lib/commands/HSET.spec.ts @@ -1,74 +1,71 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(HSET, '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( + parseArgs(HSET, '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( + parseArgs(HSET, 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( + parseArgs(HSET, '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( + parseArgs(HSET, '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( + parseArgs(HSET, '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( + parseArgs(HSET, '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..1f50aeacf03 100644 --- a/packages/client/lib/commands/HSET.ts +++ b/packages/client/lib/commands/HSET.ts @@ -1,73 +1,74 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + parseCommand(parser: CommandParser, ...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) { + parser.push('HSET'); + parser.pushKey(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) { + 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(parser: CommandParser, map: HSETMap): void { + for (const [key, value] of map.entries()) { + parser.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(parser: CommandParser, tuples: HSETTuples): void { + for (const tuple of tuples) { + if (Array.isArray(tuple)) { + pushTuples(parser, 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)); - } + parser.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(parser: CommandParser, object: HSETObject): void { + for (const key of Object.keys(object)) { + parser.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/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/HSETNX.spec.ts b/packages/client/lib/commands/HSETNX.spec.ts index 190fa50ae97..e65f9fb219c 100644 --- a/packages/client/lib/commands/HSETNX.spec.ts +++ b/packages/client/lib/commands/HSETNX.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HSETNX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 'value'), - ['HSETNX', 'key', 'field', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HSETNX, '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..130d7cd81d3 100644 --- a/packages/client/lib/commands/HSETNX.ts +++ b/packages/client/lib/commands/HSETNX.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command, NumberReply } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { - return ['HSETNX', key, field, value]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + field: RedisArgument, + value: RedisArgument + ) { + 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 79c3150211e..47dd0eaf795 100644 --- a/packages/client/lib/commands/HSTRLEN.spec.ts +++ b/packages/client/lib/commands/HSTRLEN.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HSTRLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HSTRLEN', 'key', 'field'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(HSTRLEN, '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..2468747d4c9 100644 --- a/packages/client/lib/commands/HSTRLEN.ts +++ b/packages/client/lib/commands/HSTRLEN.ts @@ -1,12 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { - return ['HSTRLEN', key, field]; -} - -export declare function transformReply(): number; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 21b8b329a5d..a79500e4d06 100644 --- a/packages/client/lib/commands/HTTL.spec.ts +++ b/packages/client/lib/commands/HTTL.spec.ts @@ -1,7 +1,8 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('HTTL', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,18 +10,17 @@ describe('HTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + parseArgs(HTTL, 'key', 'field'), ['HTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - 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 d3eedd0db0e..4b8fe5d7e85 100644 --- a/packages/client/lib/commands/HTTL.ts +++ b/packages/client/lib/commands/HTTL.ts @@ -1,11 +1,18 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { 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 { + IS_READ_ONLY: true, + 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 d0a6c39ce5f..89cbb52861c 100644 --- a/packages/client/lib/commands/HVALS.spec.ts +++ b/packages/client/lib/commands/HVALS.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('HVALS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HVALS', 'key'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(HVALS, '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..ab17e47f533 100644 --- a/packages/client/lib/commands/HVALS.ts +++ b/packages/client/lib/commands/HVALS.ts @@ -1,9 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['HVALS', key]; -} - -export declare function transformReply(): Array; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 321d83edc54..0fe7ed7f8e6 100644 --- a/packages/client/lib/commands/INCR.spec.ts +++ b/packages/client/lib/commands/INCR.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('INCR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['INCR', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INCR, '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..e719f06bc19 100644 --- a/packages/client/lib/commands/INCR.ts +++ b/packages/client/lib/commands/INCR.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['INCR', key]; -} - -export declare function transformReply(): number; +export default { + 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 a671d0ec259..e2a5842f20a 100644 --- a/packages/client/lib/commands/INCRBY.spec.ts +++ b/packages/client/lib/commands/INCRBY.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; -describe('INCR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['INCRBY', 'key', '1'] - ); - }); +describe('INCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INCRBY, '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..bf463185044 100644 --- a/packages/client/lib/commands/INCRBY.ts +++ b/packages/client/lib/commands/INCRBY.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - increment: number -): RedisCommandArguments { - return ['INCRBY', key, increment.toString()]; -} - -export declare function transformReply(): number; +export default { + 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 b2dd5aa5da9..57596970708 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('INCRBYFLOAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1.5), - ['INCRBYFLOAT', 'key', '1.5'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INCRBYFLOAT, '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..9a2dba42a6e 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - increment: number -): RedisCommandArguments { - return ['INCRBYFLOAT', key, increment.toString()]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + 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 118682c7da1..7ee8a95c137 100644 --- a/packages/client/lib/commands/INFO.spec.ts +++ b/packages/client/lib/commands/INFO.spec.ts @@ -1,20 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('INFO', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['INFO'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(INFO), + ['INFO'] + ); + }); - it('server section', () => { - assert.deepEqual( - transformArguments('server'), - ['INFO', 'server'] - ); - }); + it('server section', () => { + assert.deepEqual( + parseArgs(INFO, '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..82cbd497a5b 100644 --- a/packages/client/lib/commands/INFO.ts +++ b/packages/client/lib/commands/INFO.ts @@ -1,13 +1,15 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; -export function transformArguments(section?: string): Array { - const args = ['INFO']; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, section?: RedisArgument) { + parser.push('INFO'); if (section) { - args.push(section); + parser.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..e516245d2ee 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(pattern: RedisCommandArgument): RedisCommandArguments { - return ['KEYS', pattern]; -} - -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 a6b4863f39e..fba26811170 100644 --- a/packages/client/lib/commands/LASTSAVE.spec.ts +++ b/packages/client/lib/commands/LASTSAVE.spec.ts @@ -1,16 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LASTSAVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LASTSAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LASTSAVE), + ['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..447cb95ab6d 100644 --- a/packages/client/lib/commands/LASTSAVE.ts +++ b/packages/client/lib/commands/LASTSAVE.ts @@ -1,9 +1,11 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['LASTSAVE']; -} - -export function transformReply(reply: number): Date { - return new Date(reply); -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 3888ff8bd36..654751b5b57 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LATENCY DOCTOR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LATENCY', 'DOCTOR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LATENCY_DOCTOR), + ['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..49c830b3065 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['LATENCY', 'DOCTOR']; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 21755a253b3..7135dc1c420 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts @@ -1,28 +1,27 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LATENCY GRAPH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('command'), - [ - 'LATENCY', - 'GRAPH', - 'command' - ] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LATENCY_GRAPH, '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..20251c3cded 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.ts @@ -1,25 +1,32 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 e79e969b261..64f94d0d1a3 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts @@ -1,26 +1,27 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LATENCY HISTORY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('command'), - ['LATENCY', 'HISTORY', 'command'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LATENCY_HISTORY, '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..6e0e4d5c560 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.ts @@ -1,27 +1,34 @@ -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 { CommandParser } from '../client/parser'; +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) { - return ['LATENCY', 'HISTORY', event]; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, event: LatencyEventType) { + parser.push('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..2cd2d9a5e06 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.spec.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.spec.ts @@ -1,27 +1,28 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LATENCY LATEST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LATENCY', 'LATEST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LATENCY_LATEST), + ['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..2ce3efd291c 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.ts @@ -1,12 +1,17 @@ -import { RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['LATENCY', 'LATEST']; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..aedbb1b34e3 100644 --- a/packages/client/lib/commands/LCS.spec.ts +++ b/packages/client/lib/commands/LCS.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LCS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LCS, '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..ed4f11ad990 100644 --- a/packages/client/lib/commands/LCS.ts +++ b/packages/client/lib/commands/LCS.ts @@ -1,18 +1,15 @@ -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 { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key1: RedisArgument, + key2: RedisArgument + ) { + 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 fc3ee54f7c0..c4cc6681d85 100644 --- a/packages/client/lib/commands/LCS_IDX.spec.ts +++ b/packages/client/lib/commands/LCS_IDX.spec.ts @@ -1,41 +1,35 @@ -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'; +import { parseArgs } from './generic-transformers'; -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( + parseArgs(LCS_IDX, '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..cb0a6b07657 100644 --- a/packages/client/lib/commands/LCS_IDX.ts +++ b/packages/client/lib/commands/LCS_IDX.ts @@ -1,42 +1,49 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { CommandParser } from '../client/parser'; +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 function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - const args = transformLcsArguments(key1, key2); - args.push('IDX'); - return args; +export interface LcsIdxOptions { + MINMATCHLEN?: number; } -type RawReply = [ - 'matches', - Array<[ - key1: RawRangeReply, - key2: RawRangeReply - ]>, - 'len', - number -]; +export type LcsIdxRange = TuplesReply<[ + start: NumberReply, + end: NumberReply +]>; -interface Reply { - matches: Array<{ - key1: RangeReply; - key2: RangeReply; - }>; - length: number; -} +export type LcsIdxMatches = ArrayReply< + TuplesReply<[ + key1: LcsIdxRange, + key2: LcsIdxRange + ]> +>; -export function transformReply(reply: RawReply): Reply { - return { - matches: reply[1].map(([key1, key2]) => ({ - key1: transformRangeReply(key1), - key2: transformRangeReply(key2) - })), - length: reply[3] - }; -} +export type LcsIdxReply = TuplesToMapReply<[ + [BlobStringReply<'matches'>, LcsIdxMatches], + [BlobStringReply<'len'>, NumberReply] +]>; + +export default { + IS_READ_ONLY: LCS.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + key1: RedisArgument, + key2: RedisArgument, + options?: LcsIdxOptions + ) { + LCS.parseCommand(parser, key1, key2); + + parser.push('IDX'); + + if (options?.MINMATCHLEN) { + parser.push('MINMATCHLEN', options.MINMATCHLEN.toString()); + } + }, + 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..92ecad4761c 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts @@ -1,42 +1,35 @@ -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'; +import { parseArgs } from './generic-transformers'; -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( + parseArgs(LCS_IDX_WITHMATCHLEN, '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..d2a743983e1 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts @@ -1,45 +1,31 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { TuplesToMapReply, BlobStringReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +import LCS_IDX, { 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; -} +export type LcsIdxWithMatchLenReply = TuplesToMapReply<[ + [BlobStringReply<'matches'>, LcsIdxWithMatchLenMatches], + [BlobStringReply<'len'>, NumberReply] +]>; -type RawReply = [ - 'matches', - Array<[ - key1: RawRangeReply, - key2: RawRangeReply, - length: number - ]>, - 'len', - number -]; - -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 { + IS_READ_ONLY: LCS_IDX.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; + LCS_IDX.parseCommand(...args); + parser.push('WITHMATCHLEN'); + }, + 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..53a2e83c326 100644 --- a/packages/client/lib/commands/LCS_LEN.spec.ts +++ b/packages/client/lib/commands/LCS_LEN.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LCS_LEN', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2', 'LEN'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LCS_LEN, '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..a1f92d914a4 100644 --- a/packages/client/lib/commands/LCS_LEN.ts +++ b/packages/client/lib/commands/LCS_LEN.ts @@ -1,15 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { NumberReply, Command } from '../RESP/types'; +import LCS from './LCS'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS'; +export default { + IS_READ_ONLY: LCS.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; -export function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - const args = transformLcsArguments(key1, key2); - args.push('LEN'); - return args; -} - -export declare function transformReply(): number; + 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 aa3aafa789b..41eff474a1a 100644 --- a/packages/client/lib/commands/LINDEX.spec.ts +++ b/packages/client/lib/commands/LINDEX.spec.ts @@ -1,36 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; - 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( + parseArgs(LINDEX, '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..6335fc40c2c 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -1,14 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - index: number -): RedisCommandArguments { - return ['LINDEX', key, index.toString()]; -} - -export declare function transformReply(): RedisCommandArgument | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 6454cc48536..c3c89d56c12 100644 --- a/packages/client/lib/commands/LINSERT.spec.ts +++ b/packages/client/lib/commands/LINSERT.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LINSERT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'BEFORE', 'pivot', 'element'), - ['LINSERT', 'key', 'BEFORE', 'pivot', 'element'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LINSERT, '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..8a40ac66630 100644 --- a/packages/client/lib/commands/LINSERT.ts +++ b/packages/client/lib/commands/LINSERT.ts @@ -1,22 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; type LInsertPosition = 'BEFORE' | 'AFTER'; -export function transformArguments( - key: RedisCommandArgument, +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, position: LInsertPosition, - pivot: RedisCommandArgument, - element: RedisCommandArgument -): RedisCommandArguments { - return [ - 'LINSERT', - key, - position, - pivot, - element - ]; -} - -export declare function transformReply(): number; + pivot: RedisArgument, + element: RedisArgument + ) { + 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 fb126ddad55..d86078d0b48 100644 --- a/packages/client/lib/commands/LLEN.spec.ts +++ b/packages/client/lib/commands/LLEN.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['LLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LLEN, '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..674e022e60d 100644 --- a/packages/client/lib/commands/LLEN.ts +++ b/packages/client/lib/commands/LLEN.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['LLEN', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 f1d418c394e..bed3ff8eab0 100644 --- a/packages/client/lib/commands/LMOVE.spec.ts +++ b/packages/client/lib/commands/LMOVE.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LMOVE, '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..f3ac847e900 100644 --- a/packages/client/lib/commands/LMOVE.ts +++ b/packages/client/lib/commands/LMOVE.ts @@ -1,21 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + source: RedisArgument, + destination: RedisArgument, sourceSide: ListSide, destinationSide: ListSide -): RedisCommandArguments { - return [ - 'LMOVE', - source, - destination, - sourceSide, - destinationSide, - ]; -} - -export declare function transformReply(): RedisCommandArgument | null; + ) { + 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 5675ee9a285..bd2cf869e74 100644 --- a/packages/client/lib/commands/LMPOP.spec.ts +++ b/packages/client/lib/commands/LMPOP.spec.ts @@ -1,32 +1,51 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LMPOP, '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( + parseArgs(LMPOP, '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..c8095e42e75 100644 --- a/packages/client/lib/commands/LMPOP.ts +++ b/packages/client/lib/commands/LMPOP.ts @@ -1,22 +1,35 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; +import { ListSide, RedisVariadicArgument, Tail } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; +export interface LMPopOptions { + COUNT?: number; +} + +export function parseLMPopArguments( + parser: CommandParser, + keys: RedisVariadicArgument, + side: ListSide, + options?: LMPopOptions +) { + parser.pushKeysLength(keys); + parser.push(side); -export function transformArguments( - keys: RedisCommandArgument | Array, - side: ListSide, - options?: LMPopOptions -): RedisCommandArguments { - return transformLMPopArguments( - ['LMPOP'], - keys, - side, - options - ); + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); + } } -export declare function transformReply(): null | [ - key: string, - elements: Array -]; +export type LMPopArguments = Tail>; + +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, ...args: LMPopArguments) { + parser.push('LMPOP'); + parseLMPopArguments(parser, ...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..b06030b0d0e 100644 --- a/packages/client/lib/commands/LOLWUT.spec.ts +++ b/packages/client/lib/commands/LOLWUT.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LOLWUT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['LOLWUT'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(LOLWUT), + ['LOLWUT'] + ); + }); - it('with version', () => { - assert.deepEqual( - transformArguments(5), - ['LOLWUT', 'VERSION', '5'] - ); - }); + it('with version', () => { + assert.deepEqual( + parseArgs(LOLWUT, 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( + parseArgs(LOLWUT, 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..372bf536967 100644 --- a/packages/client/lib/commands/LOLWUT.ts +++ b/packages/client/lib/commands/LOLWUT.ts @@ -1,19 +1,18 @@ -import { RedisCommandArgument } from '.'; - -export const IS_READ_ONLY = true; - -export function transformArguments(version?: number, ...optionalArguments: Array): Array { - const args = ['LOLWUT']; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, version?: number, ...optionalArguments: Array) { + parser.push('LOLWUT'); if (version) { - args.push( - 'VERSION', - version.toString(), - ...optionalArguments.map(String), - ); + parser.push( + 'VERSION', + version.toString() + ); + parser.pushVariadic(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..93449bdbf5f 100644 --- a/packages/client/lib/commands/LPOP.spec.ts +++ b/packages/client/lib/commands/LPOP.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LPOP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['LPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LPOP, '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..3125236bfa0 100644 --- a/packages/client/lib/commands/LPOP.ts +++ b/packages/client/lib/commands/LPOP.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['LPOP', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + 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 9d87fad3862..04bb3648d0a 100644 --- a/packages/client/lib/commands/LPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOP_COUNT.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LPOP_COUNT, '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..6d9aba42c21 100644 --- a/packages/client/lib/commands/LPOP_COUNT.ts +++ b/packages/client/lib/commands/LPOP_COUNT.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NullReply, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import LPOP from './LPOP'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return ['LPOP', key, count.toString()]; -} - -export declare function transformReply(): Array | null; +export default { + IS_READ_ONLY: false, + 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 6b6050f2c3b..f26af3f540f 100644 --- a/packages/client/lib/commands/LPOS.spec.ts +++ b/packages/client/lib/commands/LPOS.spec.ts @@ -1,58 +1,55 @@ -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'; +import { parseArgs } from './generic-transformers'; 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('processCommand', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(LPOS, '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( + parseArgs(LPOS, '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( + parseArgs(LPOS, '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( + parseArgs(LPOS, '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..bb05ba6555d 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 { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + element: RedisArgument, options?: LPosOptions -): RedisCommandArguments { - const args = ['LPOS', key, element]; + ) { + parser.push('LPOS'); + parser.pushKey(key); + parser.push(element); - if (typeof options?.RANK === 'number') { - args.push('RANK', options.RANK.toString()); + if (options?.RANK !== undefined) { + parser.push('RANK', options.RANK.toString()); } - if (typeof options?.MAXLEN === 'number') { - args.push('MAXLEN', options.MAXLEN.toString()); + if (options?.MAXLEN !== undefined) { + parser.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..702ef5a746b 100644 --- a/packages/client/lib/commands/LPOS_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOS_COUNT.spec.ts @@ -1,58 +1,55 @@ -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'; +import { parseArgs } from './generic-transformers'; 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('processCommand', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(LPOS_COUNT, '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( + parseArgs(LPOS_COUNT, '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( + parseArgs(LPOS_COUNT, 'key', 'element', 0, { + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'MAXLEN', '10', 'COUNT', '0'] + ); }); - testUtils.testWithClient('client.lPosCount', async client => { - assert.deepEqual( - await client.lPosCount('key', 'element', 0), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('with RANK, MAXLEN', () => { + assert.deepEqual( + parseArgs(LPOS_COUNT, 'key', 'element', 0, { + RANK: 0, + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10', 'COUNT', '0'] + ); + }); + }); - 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..e782a2d26ee 100644 --- a/packages/client/lib/commands/LPOS_COUNT.ts +++ b/packages/client/lib/commands/LPOS_COUNT.ts @@ -1,27 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { LPosOptions } from './LPOS'; +import { CommandParser } from '../client/parser'; +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 { + CACHEABLE: LPOS.CACHEABLE, + IS_READ_ONLY: LPOS.IS_READ_ONLY, + parseCommand( + parser: CommandParser, + 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()); - } - - args.push('COUNT', count.toString()); - - if (typeof options?.MAXLEN === 'number') { - args.push('MAXLEN', options.MAXLEN.toString()); - } - - return args; -} + ) { + LPOS.parseCommand(parser, key, element, options); -export declare function transformReply(): Array; + 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 b5b1f5084eb..09c7d9da772 100644 --- a/packages/client/lib/commands/LPUSH.spec.ts +++ b/packages/client/lib/commands/LPUSH.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LPUSH, '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( + parseArgs(LPUSH, '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..293029034ee 100644 --- a/packages/client/lib/commands/LPUSH.ts +++ b/packages/client/lib/commands/LPUSH.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 d978e5a588f..179a0ddb29e 100644 --- a/packages/client/lib/commands/LPUSHX.spec.ts +++ b/packages/client/lib/commands/LPUSHX.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LPUSHX, '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( + parseArgs(LPUSHX, '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..98dd51a7ac2 100644 --- a/packages/client/lib/commands/LPUSHX.ts +++ b/packages/client/lib/commands/LPUSHX.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 dffe6087b80..c0bb046d898 100644 --- a/packages/client/lib/commands/LRANGE.spec.ts +++ b/packages/client/lib/commands/LRANGE.spec.ts @@ -1,27 +1,23 @@ - -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'; +import { parseArgs } from './generic-transformers'; 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('processCommand', () => { + assert.deepEqual( + parseArgs(LRANGE, '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..ab033dd88a4 100644 --- a/packages/client/lib/commands/LRANGE.ts +++ b/packages/client/lib/commands/LRANGE.ts @@ -1,20 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - start: number, - stop: number -): RedisCommandArguments { - return [ - 'LRANGE', - key, - start.toString(), - stop.toString() - ]; -} - -export declare function transformReply(): Array; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 3405f4beb07..2a36d8ee2f1 100644 --- a/packages/client/lib/commands/LREM.spec.ts +++ b/packages/client/lib/commands/LREM.spec.ts @@ -1,27 +1,23 @@ - -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(LREM, '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..bb97e3882e7 100644 --- a/packages/client/lib/commands/LREM.ts +++ b/packages/client/lib/commands/LREM.ts @@ -1,18 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count: number, - element: RedisCommandArgument -): RedisCommandArguments { - return [ - 'LREM', - key, - count.toString(), - element - ]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 d7241032cc6..c7522942402 100644 --- a/packages/client/lib/commands/LSET.spec.ts +++ b/packages/client/lib/commands/LSET.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 './LSET'; +import LSET from './LSET'; +import { parseArgs } from './generic-transformers'; describe('LSET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 'element'), - ['LSET', 'key', '0', 'element'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LSET, '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..0fe646fbb73 100644 --- a/packages/client/lib/commands/LSET.ts +++ b/packages/client/lib/commands/LSET.ts @@ -1,18 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - index: number, - element: RedisCommandArgument -): RedisCommandArguments { - return [ - 'LSET', - key, - index.toString(), - element - ]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + IS_READ_ONLY: true, + 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 5b6ac5d3660..5b6d77c91de 100644 --- a/packages/client/lib/commands/LTRIM.spec.ts +++ b/packages/client/lib/commands/LTRIM.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('LTRIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, -1), - ['LTRIM', 'key', '0', '-1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(LTRIM, '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..acc7e767d0d 100644 --- a/packages/client/lib/commands/LTRIM.ts +++ b/packages/client/lib/commands/LTRIM.ts @@ -1,18 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - start: number, - stop: number -): RedisCommandArguments { - return [ - 'LTRIM', - key, - start.toString(), - stop.toString() - ]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + 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 ad97047606c..9d822f8e07e 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MEMORY DOCTOR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'DOCTOR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MEMORY_DOCTOR), + ['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..3a2d808db10 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['MEMORY', 'DOCTOR']; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 ce866f1e116..a4a85f5b994 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MEMORY MALLOC-STATS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'MALLOC-STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MEMORY_MALLOC_STATS), + ['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..af6b5db3347 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts @@ -1,5 +1,12 @@ -export function transformArguments(): Array { - return ['MEMORY', 'MALLOC-STATS']; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..be5fb738b0a 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MEMORY PURGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'PURGE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MEMORY_PURGE), + ['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..bbd02890786 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.ts @@ -1,5 +1,12 @@ -export function transformArguments(): Array { - return ['MEMORY', 'PURGE']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser) { + parser.push('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..6aad05116af 100644 --- a/packages/client/lib/commands/MEMORY_STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_STATS.spec.ts @@ -1,108 +1,47 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MEMORY STATS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MEMORY_STATS), + ['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..33410535aa9 100644 --- a/packages/client/lib/commands/MEMORY_STATS.ts +++ b/packages/client/lib/commands/MEMORY_STATS.ts @@ -1,93 +1,69 @@ -export function transformArguments(): Array { - return ['MEMORY', 'STATS']; -} +import { CommandParser } from '../client/parser'; +import { TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -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; - }; - }; -} +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] +]>; -const FIELDS_MAPPING = { - 'peak.allocated': 'peakAllocated', - 'total.allocated': 'totalAllocated', - 'startup.allocated': 'startupAllocated', - 'replication.backlog': 'replicationBacklog', - 'clients.slaves': 'clientsReplicas', - 'clients.normal': 'clientsNormal', - 'aof.buffer': 'aofBuffer', - 'lua.caches': 'luaCaches', - 'overhead.total': 'overheadTotal', - 'keys.count': 'keysCount', - 'keys.bytes-per-key': 'keysBytesPerKey', - 'dataset.bytes': 'datasetBytes', - 'dataset.percentage': 'datasetPercentage', - 'peak.percentage': 'peakPercentage', - 'allocator.allocated': 'allocatorAllocated', - 'allocator.active': 'allocatorActive', - 'allocator.resident': 'allocatorResident', - 'allocator-fragmentation.ratio': 'allocatorFragmentationRatio', - 'allocator-fragmentation.bytes': 'allocatorFragmentationBytes', - 'allocator-rss.ratio': 'allocatorRssRatio', - 'allocator-rss.bytes': 'allocatorRssBytes', - 'rss-overhead.ratio': 'rssOverheadRatio', - 'rss-overhead.bytes': 'rssOverheadBytes', - 'fragmentation': 'fragmentation', - 'fragmentation.bytes': 'fragmentationBytes' - }, - DB_FIELDS_MAPPING = { - 'overhead.hashtable.main': 'overheadHashtableMain', - 'overhead.hashtable.expires': 'overheadHashtableExpires' - }; - -export function transformReply(rawReply: Array>): MemoryStatsReply { - const reply: any = { - db: {} - }; - - for (let i = 0; i < rawReply.length; i += 2) { - const key = rawReply[i] as string; - if (key.startsWith('db.')) { - const dbTuples = rawReply[i + 1] as Array, - db: any = {}; - for (let j = 0; j < dbTuples.length; j += 2) { - db[DB_FIELDS_MAPPING[dbTuples[j] as keyof typeof DB_FIELDS_MAPPING]] = dbTuples[j + 1]; - } +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..edf673564ee 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MEMORY USAGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['MEMORY', 'USAGE', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(MEMORY_USAGE, 'key'), + ['MEMORY', 'USAGE', 'key'] + ); + }); - it('with SAMPLES', () => { - assert.deepEqual( - transformArguments('key', { - SAMPLES: 1 - }), - ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1'] - ); - }); + it('with SAMPLES', () => { + assert.deepEqual( + parseArgs(MEMORY_USAGE, '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..6e85438dbed 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.ts @@ -1,19 +1,19 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +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 { - const args = ['MEMORY', 'USAGE', key]; +export default { + IS_READ_ONLY: true, + 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; -} - -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..048fa6f0a58 100644 --- a/packages/client/lib/commands/MGET.spec.ts +++ b/packages/client/lib/commands/MGET.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['MGET', '1', '2'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(MGET, ['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..ce1e9ba7781 100644 --- a/packages/client/lib/commands/MGET.ts +++ b/packages/client/lib/commands/MGET.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: Array -): RedisCommandArguments { - return ['MGET', ...keys]; -} - -export declare function transformReply(): Array; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 ca7ceb48b39..dd2fbdc82ff 100644 --- a/packages/client/lib/commands/MIGRATE.spec.ts +++ b/packages/client/lib/commands/MIGRATE.spec.ts @@ -1,76 +1,77 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MIGRATE'; +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( - 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( + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10'] + ); + }); + + it('multiple keys', () => { + assert.deepEqual( + parseArgs(MIGRATE, '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( + parseArgs(MIGRATE, '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( + parseArgs(MIGRATE, '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( + parseArgs(MIGRATE, '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( + parseArgs(MIGRATE, '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( + parseArgs(MIGRATE, '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..15345060aa7 100644 --- a/packages/client/lib/commands/MIGRATE.ts +++ b/packages/client/lib/commands/MIGRATE.ts @@ -1,65 +1,65 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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, + parseCommand( + parser: CommandParser, + 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); - + ) { + 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( - destinationDb.toString(), - timeout.toString() + + 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( - 'AUTH2', - options.AUTH.username, - options.AUTH.password - ); - } else { - args.push( - 'AUTH', - options.AUTH.password - ); - } + if (options.AUTH.username) { + parser.push( + 'AUTH2', + options.AUTH.username, + options.AUTH.password + ); + } else { + parser.push( + 'AUTH', + options.AUTH.password + ); + } } - + if (isKeyArray) { - args.push( - 'KEYS', - ...key - ); + parser.push('KEYS'); + parser.pushVariadic(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..0aab973cf21 100644 --- a/packages/client/lib/commands/MODULE_LIST.spec.ts +++ b/packages/client/lib/commands/MODULE_LIST.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_LIST'; +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( - transformArguments(), - ['MODULE', 'LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MODULE_LIST), + ['MODULE', 'LIST'] + ); + }); }); diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts index d75b2428308..85203138f57 100644 --- a/packages/client/lib/commands/MODULE_LIST.ts +++ b/packages/client/lib/commands/MODULE_LIST.ts @@ -1,5 +1,27 @@ -export function transformArguments(): Array { - return ['MODULE', 'LIST']; -} +import { CommandParser } from '../client/parser'; +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export type ModuleListReply = ArrayReply, BlobStringReply], + [BlobStringReply<'ver'>, NumberReply], +]>>; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..418dd9b5daf 100644 --- a/packages/client/lib/commands/MODULE_LOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_LOAD.spec.ts @@ -1,20 +1,21 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_LOAD'; +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( - transformArguments('path'), - ['MODULE', 'LOAD', 'path'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(MODULE_LOAD, '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( + 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 b44b4b57ce6..ceb90c1c353 100644 --- a/packages/client/lib/commands/MODULE_LOAD.ts +++ b/packages/client/lib/commands/MODULE_LOAD.ts @@ -1,11 +1,15 @@ -export function transformArguments(path: string, moduleArgs?: Array): Array { - const args = ['MODULE', 'LOAD', path]; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; - if (moduleArgs) { - args.push(...moduleArgs); - } - - return args; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, path: RedisArgument, moduleArguments?: Array) { + parser.push('MODULE', 'LOAD', path); -export declare function transformReply(): string; + if (moduleArguments) { + parser.push(...moduleArguments); + } + }, + 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..581f41e03c8 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_UNLOAD'; +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( - transformArguments('name'), - ['MODULE', 'UNLOAD', 'name'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + 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 d5927778fe6..1acc359d0d4 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.ts @@ -1,5 +1,11 @@ -export function transformArguments(name: string): Array { - return ['MODULE', 'UNLOAD', name]; -} +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 f7fdc481cbf..91a01378b22 100644 --- a/packages/client/lib/commands/MOVE.spec.ts +++ b/packages/client/lib/commands/MOVE.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('MOVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['MOVE', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MOVE, '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..8a6c5427fbc 100644 --- a/packages/client/lib/commands/MOVE.ts +++ b/packages/client/lib/commands/MOVE.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export function transformArguments(key: string, db: number): Array { - return ['MOVE', key, db.toString()]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + 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 0568f38487e..cfb14eceb05 100644 --- a/packages/client/lib/commands/MSET.spec.ts +++ b/packages/client/lib/commands/MSET.spec.ts @@ -1,42 +1,39 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(MSET, ['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( + parseArgs(MSET, [['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( + parseArgs(MSET, { 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..f761854f09c 100644 --- a/packages/client/lib/commands/MSET.ts +++ b/packages/client/lib/commands/MSET.ts @@ -1,24 +1,41 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export type MSetArguments = - Array<[RedisCommandArgument, RedisCommandArgument]> | - Array | - Record; - -export function transformArguments(toSet: MSetArguments): RedisCommandArguments { - const args: RedisCommandArguments = ['MSET']; + Array<[RedisArgument, RedisArgument]> | + Array | + Record; - if (Array.isArray(toSet)) { - args.push(...toSet.flat()); +export function parseMSetArguments(parser: CommandParser, toSet: MSetArguments) { + if (Array.isArray(toSet)) { + 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 { - for (const key of Object.keys(toSet)) { - args.push(key, toSet[key]); - } + const arr = toSet as Array; + for (let i=0; i < arr.length; i += 2) { + parser.pushKey(arr[i]); + parser.push(arr[i+1]); + } } - - return args; + } else { + for (const tuple of Object.entries(toSet)) { + parser.pushKey(tuple[0]); + parser.push(tuple[1]); + } + } } -export declare function transformReply(): RedisCommandArgument; +export default { + IS_READ_ONLY: true, + 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 854a9affd8a..0a9f636abc7 100644 --- a/packages/client/lib/commands/MSETNX.spec.ts +++ b/packages/client/lib/commands/MSETNX.spec.ts @@ -1,42 +1,39 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(MSETNX, ['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( + parseArgs(MSETNX, [['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( + parseArgs(MSETNX, { 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..3ecce9525de 100644 --- a/packages/client/lib/commands/MSETNX.ts +++ b/packages/client/lib/commands/MSETNX.ts @@ -1,20 +1,12 @@ -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 { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { MSetArguments, parseMSetArguments } from './MSET'; + +export default { + IS_READ_ONLY: true, + 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 6f42969d547..34f82be9b8d 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('OBJECT ENCODING', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'ENCODING', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(OBJECT_ENCODING, '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..3a795f6fb64 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['OBJECT', 'ENCODING', key]; -} - -export declare function transformReply(): string | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 6d2513cf18c..081501b12e6 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.spec.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('OBJECT FREQ', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'FREQ', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(OBJECT_FREQ, '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..dad1124b101 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['OBJECT', 'FREQ', key]; -} - -export declare function transformReply(): number | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 61529e1366b..30d47b8133f 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('OBJECT IDLETIME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'IDLETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(OBJECT_IDLETIME, '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..2bd32f4e65d 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['OBJECT', 'IDLETIME', key]; -} - -export declare function transformReply(): number | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 199dca3fe82..8bac08a2e5b 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('OBJECT REFCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'REFCOUNT', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(OBJECT_REFCOUNT, '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..4bee4dea60c 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['OBJECT', 'REFCOUNT', key]; -} - -export declare function transformReply(): number | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 4e53bd85a6c..fff6d7b3a76 100644 --- a/packages/client/lib/commands/PERSIST.spec.ts +++ b/packages/client/lib/commands/PERSIST.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PERSIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PERSIST', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PERSIST, '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..a1d31523664 100644 --- a/packages/client/lib/commands/PERSIST.ts +++ b/packages/client/lib/commands/PERSIST.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['PERSIST', key]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + 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 03bde656103..368bc9b4907 100644 --- a/packages/client/lib/commands/PEXPIRE.spec.ts +++ b/packages/client/lib/commands/PEXPIRE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PEXPIRE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 1), - ['PEXPIRE', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(PEXPIRE, '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( + parseArgs(PEXPIRE, '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..4053f46c8e2 100644 --- a/packages/client/lib/commands/PEXPIRE.ts +++ b/packages/client/lib/commands/PEXPIRE.ts @@ -1,19 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - milliseconds: number, +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + ms: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - const args = ['PEXPIRE', key, milliseconds.toString()]; + ) { + parser.push('PEXPIRE'); + parser.pushKey(key); + parser.push(ms.toString()); if (mode) { - args.push(mode); + parser.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..f1053920403 100644 --- a/packages/client/lib/commands/PEXPIREAT.spec.ts +++ b/packages/client/lib/commands/PEXPIREAT.spec.ts @@ -1,36 +1,40 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PEXPIREAT', () => { - describe('transformArguments', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', 1), - ['PEXPIREAT', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + parseArgs(PEXPIREAT, '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( + parseArgs(PEXPIREAT, '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( + parseArgs(PEXPIREAT, '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..e454447c970 100644 --- a/packages/client/lib/commands/PEXPIREAT.ts +++ b/packages/client/lib/commands/PEXPIREAT.ts @@ -1,24 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + msTimestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - const args = [ - 'PEXPIREAT', - key, - transformPXAT(millisecondsTimestamp) - ]; + ) { + parser.push('PEXPIREAT'); + parser.pushKey(key); + parser.push(transformPXAT(msTimestamp)); if (mode) { - args.push(mode); + parser.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..dbfc69e80dc 100644 --- a/packages/client/lib/commands/PEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/PEXPIRETIME.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 './PEXPIRETIME'; +import PEXPIRETIME from './PEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('PEXPIRETIME', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PEXPIRETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PEXPIRETIME, '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..b5d04eae230 100644 --- a/packages/client/lib/commands/PEXPIRETIME.ts +++ b/packages/client/lib/commands/PEXPIRETIME.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['PEXPIRETIME', key]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 8c0e752fd50..55c4311e638 100644 --- a/packages/client/lib/commands/PFADD.spec.ts +++ b/packages/client/lib/commands/PFADD.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PFADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'element'), - ['PFADD', 'key', 'element'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(PFADD, 'key', 'element'), + ['PFADD', 'key', 'element'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['PFADD', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(PFADD, '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..94c2d1d5ae6 100644 --- a/packages/client/lib/commands/PFADD.ts +++ b/packages/client/lib/commands/PFADD.ts @@ -1,13 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['PFADD', key], element); -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: true, + 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 a1ea06c4494..aec2ebecf0b 100644 --- a/packages/client/lib/commands/PFCOUNT.spec.ts +++ b/packages/client/lib/commands/PFCOUNT.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PFCOUNT', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['PFCOUNT', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(PFCOUNT, 'key'), + ['PFCOUNT', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PFCOUNT', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(PFCOUNT, ['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..46d2e2ed71f 100644 --- a/packages/client/lib/commands/PFCOUNT.ts +++ b/packages/client/lib/commands/PFCOUNT.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: true, + 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 881fc5f5439..a286e932913 100644 --- a/packages/client/lib/commands/PFMERGE.spec.ts +++ b/packages/client/lib/commands/PFMERGE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PFMERGE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'source'), - ['PFMERGE', 'destination', 'source'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(PFMERGE, 'destination', 'source'), + ['PFMERGE', 'destination', 'source'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['PFMERGE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(PFMERGE, '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..e8eccf1afff 100644 --- a/packages/client/lib/commands/PFMERGE.ts +++ b/packages/client/lib/commands/PFMERGE.ts @@ -1,10 +1,18 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(destination: string, source: string | Array): RedisCommandArguments { - return pushVerdictArguments(['PFMERGE', destination], source); -} - -export declare function transformReply(): string; +export default { + parseCommand( + parser: CommandParser, + destination: RedisArgument, + sources?: RedisVariadicArgument + ) { + 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 06cbae43a13..56f513685f4 100644 --- a/packages/client/lib/commands/PING.spec.ts +++ b/packages/client/lib/commands/PING.spec.ts @@ -1,37 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(PING), + ['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( + parseArgs(PING, '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..26807eeeba4 100644 --- a/packages/client/lib/commands/PING.ts +++ b/packages/client/lib/commands/PING.ts @@ -1,12 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(message?: RedisCommandArgument): RedisCommandArguments { - const args: RedisCommandArguments = ['PING']; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, message?: RedisArgument) { + parser.push('PING'); if (message) { - args.push(message); + parser.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..8580e2f8e9d 100644 --- a/packages/client/lib/commands/PSETEX.spec.ts +++ b/packages/client/lib/commands/PSETEX.spec.ts @@ -1,27 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PSETEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'value'), - ['PSETEX', 'key', '1', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PSETEX, '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..03a58546d67 100644 --- a/packages/client/lib/commands/PSETEX.ts +++ b/packages/client/lib/commands/PSETEX.ts @@ -1,18 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - milliseconds: number, - value: RedisCommandArgument -): RedisCommandArguments { - return [ - 'PSETEX', - key, - milliseconds.toString(), - value - ]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + 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 e65421de590..deb04bad97e 100644 --- a/packages/client/lib/commands/PTTL.spec.ts +++ b/packages/client/lib/commands/PTTL.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PTTL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PTTL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PTTL, '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..5717c51179f 100644 --- a/packages/client/lib/commands/PTTL.ts +++ b/packages/client/lib/commands/PTTL.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['PTTL', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 b2084e668ba..930adc8c4d7 100644 --- a/packages/client/lib/commands/PUBLISH.spec.ts +++ b/packages/client/lib/commands/PUBLISH.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PUBLISH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('channel', 'message'), - ['PUBLISH', 'channel', 'message'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PUBLISH, '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..557efd18834 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments( - channel: RedisCommandArgument, - message: RedisCommandArgument -): RedisCommandArguments { - return ['PUBLISH', channel, message]; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + IS_FORWARD_COMMAND: true, + 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 c427eab4850..369e339a497 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB CHANNELS', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'CHANNELS'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(PUBSUB_CHANNELS), + ['PUBSUB', 'CHANNELS'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['PUBSUB', 'CHANNELS', 'patter*'] - ); - }); + it('with pattern', () => { + assert.deepEqual( + parseArgs(PUBSUB_CHANNELS, '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..0f53c79a78a 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts @@ -1,13 +1,16 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(pattern?: string): Array { - const args = ['PUBSUB', 'CHANNELS']; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, pattern?: RedisArgument) { + parser.push('PUBSUB', 'CHANNELS'); if (pattern) { - args.push(pattern); + parser.push(pattern); } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; - return args; -} - -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..d75256bb43c 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB NUMPAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'NUMPAT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(PUBSUB_NUMPAT), + ['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..173446e023b 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts @@ -1,7 +1,11 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['PUBSUB', 'NUMPAT']; -} - -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 e35558ef865..11339ae2bb5 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB NUMSUB', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'NUMSUB'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(PUBSUB_NUMSUB), + ['PUBSUB', 'NUMSUB'] + ); + }); - it('string', () => { - assert.deepEqual( - transformArguments('channel'), - ['PUBSUB', 'NUMSUB', 'channel'] - ); - }); + it('string', () => { + assert.deepEqual( + parseArgs(PUBSUB_NUMSUB, 'channel'), + ['PUBSUB', 'NUMSUB', 'channel'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PUBSUB', 'NUMSUB', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(PUBSUB_NUMSUB, ['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..cc74d5d8a73 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts @@ -1,24 +1,24 @@ -import { pushVerdictArguments } from './generic-transformers'; -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export const IS_READ_ONLY = true; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { + parser.push('PUBSUB', 'NUMSUB'); -export function transformArguments( - channels?: Array | RedisCommandArgument -): RedisCommandArguments { - const args = ['PUBSUB', 'NUMSUB']; - - if (channels) return pushVerdictArguments(args, channels); - - return args; -} - -export function transformReply(rawReply: Array): Record { - const transformedReply = Object.create(null); - - for (let i = 0; i < rawReply.length; i +=2) { - transformedReply[rawReply[i]] = rawReply[i + 1]; + if (channels) { + parser.pushVariadic(channels); + } + }, + 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..36597a9cfd8 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(PUBSUB_SHARDCHANNELS), + ['PUBSUB', 'SHARDCHANNELS'] + ); }); - testUtils.testWithClient('client.pubSubShardChannels', async client => { - assert.deepEqual( - await client.pubSubShardChannels(), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('with pattern', () => { + assert.deepEqual( + parseArgs(PUBSUB_SHARDCHANNELS, '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..46ac2005fc3 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts @@ -1,13 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, pattern?: RedisArgument) { + parser.push('PUBSUB', 'SHARDCHANNELS'); -export function transformArguments( - pattern?: RedisCommandArgument -): RedisCommandArguments { - const args: RedisCommandArguments = ['PUBSUB', 'SHARDCHANNELS']; - if (pattern) args.push(pattern); - return args; -} - -export declare function transformReply(): Array; + if (pattern) { + parser.push(pattern); + } + }, + 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..e335941897d 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts @@ -1,48 +1,49 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_SHARDNUMSUB'; +import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB SHARDNUMSUB', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'SHARDNUMSUB'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(PUBSUB_SHARDNUMSUB), + ['PUBSUB', 'SHARDNUMSUB'] + ); + }); - it('string', () => { - assert.deepEqual( - transformArguments('channel'), - ['PUBSUB', 'SHARDNUMSUB', 'channel'] - ); - }); + it('string', () => { + assert.deepEqual( + parseArgs(PUBSUB_SHARDNUMSUB, 'channel'), + ['PUBSUB', 'SHARDNUMSUB', 'channel'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PUBSUB', 'SHARDNUMSUB', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(PUBSUB_SHARDNUMSUB, ['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..220eadeabe3 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 { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export const IS_READ_ONLY = true; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { + parser.push('PUBSUB', 'SHARDNUMSUB'); -export function transformArguments( - channels?: Array | RedisCommandArgument -): RedisCommandArguments { - const args = ['PUBSUB', 'SHARDNUMSUB']; - - if (channels) return pushVerdictArguments(args, channels); - - return args; -} - -export function transformReply(rawReply: Array): Record { - const transformedReply = Object.create(null); - - for (let i = 0; i < rawReply.length; i += 2) { - transformedReply[rawReply[i]] = rawReply[i + 1]; + if (channels) { + parser.pushVariadic(channels); } + }, + transformReply(reply: UnwrapReply>) { + const transformedReply: Record = Object.create(null); + 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..f86617a3b75 100644 --- a/packages/client/lib/commands/RANDOMKEY.spec.ts +++ b/packages/client/lib/commands/RANDOMKEY.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('RANDOMKEY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['RANDOMKEY'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RANDOMKEY), + ['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..97d040a0d1d 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { - return ['RANDOMKEY']; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 aa4db47f81a..ac303322330 100644 --- a/packages/client/lib/commands/READONLY.spec.ts +++ b/packages/client/lib/commands/READONLY.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './READONLY'; +import { strict as assert } from 'node:assert'; +import READONLY from './READONLY'; +import { parseArgs } from './generic-transformers'; describe('READONLY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['READONLY'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(READONLY), + ['READONLY'] + ); + }); }); diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts index db7db881628..ce3300c5321 100644 --- a/packages/client/lib/commands/READONLY.ts +++ b/packages/client/lib/commands/READONLY.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['READONLY']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 6ce4a3ee56a..cc3f99a5d16 100644 --- a/packages/client/lib/commands/READWRITE.spec.ts +++ b/packages/client/lib/commands/READWRITE.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './READWRITE'; +import { strict as assert } from 'node:assert'; +import READWRITE from './READWRITE'; +import { parseArgs } from './generic-transformers'; describe('READWRITE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['READWRITE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(READWRITE), + ['READWRITE'] + ); + }); }); diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts index 60dc865e89e..7d9d8c7e00a 100644 --- a/packages/client/lib/commands/READWRITE.ts +++ b/packages/client/lib/commands/READWRITE.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['READWRITE']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 49e0af600f6..05dd9417b96 100644 --- a/packages/client/lib/commands/RENAME.spec.ts +++ b/packages/client/lib/commands/RENAME.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 './RENAME'; +import RENAME from './RENAME'; +import { parseArgs } from './generic-transformers'; describe('RENAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('from', 'to'), - ['RENAME', 'from', 'to'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RENAME, '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..245851ca31a 100644 --- a/packages/client/lib/commands/RENAME.ts +++ b/packages/client/lib/commands/RENAME.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - newKey: RedisCommandArgument -): RedisCommandArguments { - return ['RENAME', key, newKey]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + IS_READ_ONLY: true, + 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 6345eb5bd09..2367b453322 100644 --- a/packages/client/lib/commands/RENAMENX.spec.ts +++ b/packages/client/lib/commands/RENAMENX.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 './RENAMENX'; +import RENAMENX from './RENAMENX'; +import { parseArgs } from './generic-transformers'; describe('RENAMENX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('from', 'to'), - ['RENAMENX', 'from', 'to'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RENAMENX, '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..0e8d4f73cf3 100644 --- a/packages/client/lib/commands/RENAMENX.ts +++ b/packages/client/lib/commands/RENAMENX.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - newKey: RedisCommandArgument -): RedisCommandArguments { - return ['RENAMENX', key, newKey]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: true, + 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 ab1906944cf..13668639494 100644 --- a/packages/client/lib/commands/REPLICAOF.spec.ts +++ b/packages/client/lib/commands/REPLICAOF.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './REPLICAOF'; +import { strict as assert } from 'node:assert'; +import REPLICAOF from './REPLICAOF'; +import { parseArgs } from './generic-transformers'; describe('REPLICAOF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('host', 1), - ['REPLICAOF', 'host', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(REPLICAOF, 'host', 1), + ['REPLICAOF', 'host', '1'] + ); + }); }); diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts index bd452e0f371..c4b09bc4fb8 100644 --- a/packages/client/lib/commands/REPLICAOF.ts +++ b/packages/client/lib/commands/REPLICAOF.ts @@ -1,5 +1,11 @@ -export function transformArguments(host: string, port: number): Array { - return ['REPLICAOF', host, port.toString()]; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 de9fce5c628..1258cf68e2d 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.spec.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './RESTORE-ASKING'; +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( - transformArguments(), - ['RESTORE-ASKING'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RESTORE_ASKING), + ['RESTORE-ASKING'] + ); + }); }); diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts index d53d8541cd7..e8de532b6a4 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['RESTORE-ASKING']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 89d42f3d4de..6083b2eb1a5 100644 --- a/packages/client/lib/commands/RESTORE.spec.ts +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -1,74 +1,85 @@ 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'; +import { parseArgs } from './generic-transformers'; describe('RESTORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 0, 'value'), - ['RESTORE', 'key', '0', 'value'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(RESTORE, '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( + parseArgs(RESTORE, '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( + parseArgs(RESTORE, '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( + parseArgs(RESTORE, '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( + parseArgs(RESTORE, '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( + parseArgs(RESTORE, '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..49016c525bd 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -1,39 +1,41 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -interface RestoreOptions { - REPLACE?: true; - ABSTTL?: true; - IDLETIME?: number; - FREQ?: number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; + +export interface RestoreOptions { + REPLACE?: boolean; + ABSTTL?: boolean; + IDLETIME?: number; + FREQ?: number; } -export function transformArguments( - key: RedisCommandArgument, +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, ttl: number, - serializedValue: RedisCommandArgument, + serializedValue: RedisArgument, options?: RestoreOptions -): RedisCommandArguments { - 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; -} - -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..09ce6ed3427 100644 --- a/packages/client/lib/commands/ROLE.spec.ts +++ b/packages/client/lib/commands/ROLE.spec.ts @@ -1,69 +1,70 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ROLE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ROLE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ROLE), + ['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..f45bbad5c01 100644 --- a/packages/client/lib/commands/ROLE.ts +++ b/packages/client/lib/commands/ROLE.ts @@ -1,75 +1,71 @@ -export const IS_READ_ONLY = true; +import { CommandParser } from '../client/parser'; +import { BlobStringReply, NumberReply, ArrayReply, TuplesReply, UnwrapReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['ROLE']; -} +type MasterRole = [ + role: BlobStringReply<'master'>, + replicationOffest: NumberReply, + replicas: ArrayReply> +]; -interface RoleReplyInterface { - role: T; -} +type SlaveRole = [ + role: BlobStringReply<'slave'>, + masterHost: BlobStringReply, + masterPort: NumberReply, + state: BlobStringReply<'connect' | 'connecting' | 'sync' | 'connected'>, + dataReceived: NumberReply +]; -type RoleMasterRawReply = ['master', number, Array<[string, string, string]>]; +type SentinelRole = [ + role: BlobStringReply<'sentinel'>, + masterNames: ArrayReply +]; -interface RoleMasterReply extends RoleReplyInterface<'master'> { - replicationOffest: number; - replicas: Array<{ - ip: string; - port: number; - replicationOffest: number; - }>; -} +type Role = TuplesReply; -type RoleReplicaState = 'connect' | 'connecting' | 'sync' | 'connected'; - -type RoleReplicaRawReply = ['slave', string, number, RoleReplicaState, number]; - -interface RoleReplicaReply extends RoleReplyInterface<'slave'> { - master: { - ip: string; - port: number; - }; - state: RoleReplicaState; - dataReceived: number; -} - -type RoleSentinelRawReply = ['sentinel', Array]; - -interface RoleSentinelReply extends RoleReplyInterface<'sentinel'> { - masterNames: Array; -} - -type RoleRawReply = RoleMasterRawReply | RoleReplicaRawReply | RoleSentinelRawReply; - -type RoleReply = RoleMasterReply | RoleReplicaReply | RoleSentinelReply; - -export function transformReply(reply: RoleRawReply): RoleReply { - switch (reply[0]) { - case 'master': +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..844965eae1a 100644 --- a/packages/client/lib/commands/RPOP.spec.ts +++ b/packages/client/lib/commands/RPOP.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('RPOP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['RPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RPOP, '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..4cc105c3704 100644 --- a/packages/client/lib/commands/RPOP.ts +++ b/packages/client/lib/commands/RPOP.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['RPOP', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + 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 cef3049bd91..728d600bc9d 100644 --- a/packages/client/lib/commands/RPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/RPOPLPUSH.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('RPOPLPUSH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['RPOPLPUSH', 'source', 'destination'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(RPOPLPUSH, '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..dcac0472235 100644 --- a/packages/client/lib/commands/RPOPLPUSH.ts +++ b/packages/client/lib/commands/RPOPLPUSH.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument -): RedisCommandArguments { - return ['RPOPLPUSH', source, destination]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + 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 3657a608039..e055d8655b5 100644 --- a/packages/client/lib/commands/RPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/RPOP_COUNT.spec.ts @@ -1,28 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(RPOP_COUNT, '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..aff91c6a6f7 100644 --- a/packages/client/lib/commands/RPOP_COUNT.ts +++ b/packages/client/lib/commands/RPOP_COUNT.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return ['RPOP', key, count.toString()]; -} - -export declare function transformReply(): Array | null; +export default { + 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 afa5c1c6400..559fb7a2746 100644 --- a/packages/client/lib/commands/RPUSH.spec.ts +++ b/packages/client/lib/commands/RPUSH.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(RPUSH, '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( + parseArgs(RPUSH, '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..b820aae6906 100644 --- a/packages/client/lib/commands/RPUSH.ts +++ b/packages/client/lib/commands/RPUSH.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 ee2041de6f2..b9fb660c5bc 100644 --- a/packages/client/lib/commands/RPUSHX.spec.ts +++ b/packages/client/lib/commands/RPUSHX.spec.ts @@ -1,35 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(RPUSHX, '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( + parseArgs(RPUSHX, '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..243f717bb78 100644 --- a/packages/client/lib/commands/RPUSHX.ts +++ b/packages/client/lib/commands/RPUSHX.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 4533f6f9ad5..179e8602efc 100644 --- a/packages/client/lib/commands/SADD.spec.ts +++ b/packages/client/lib/commands/SADD.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SADD', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SADD, 'key', 'member'), + ['SADD', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SADD', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SADD, '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..1fb0171d8d4 100644 --- a/packages/client/lib/commands/SADD.ts +++ b/packages/client/lib/commands/SADD.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 1e1987b5ab8..5f0074f7492 100644 --- a/packages/client/lib/commands/SAVE.spec.ts +++ b/packages/client/lib/commands/SAVE.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SAVE'; +import { strict as assert } from 'node:assert'; +import SAVE from './SAVE'; +import { parseArgs } from './generic-transformers'; describe('SAVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['SAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SAVE), + ['SAVE'] + ); + }); }); diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts index 3d75c29df90..ee78884083c 100644 --- a/packages/client/lib/commands/SAVE.ts +++ b/packages/client/lib/commands/SAVE.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument } from '.'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): Array { - return ['SAVE']; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 7657b744e02..2a32cbebf4f 100644 --- a/packages/client/lib/commands/SCAN.spec.ts +++ b/packages/client/lib/commands/SCAN.spec.ts @@ -1,84 +1,63 @@ -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 { parseArgs } from './generic-transformers'; +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( + parseArgs(SCAN, '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( + parseArgs(SCAN, '0', { + MATCH: 'pattern' + }), + ['SCAN', '0', 'MATCH', 'pattern'] + ); }); - describe('transformReply', () => { - it('without keys', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - keys: [] - } - ); - }); + it('with COUNT', () => { + assert.deepEqual( + parseArgs(SCAN, '0', { + COUNT: 1 + }), + ['SCAN', '0', 'COUNT', '1'] + ); + }); - it('with keys', () => { - assert.deepEqual( - transformReply(['0', ['key']]), - { - cursor: 0, - keys: ['key'] - } - ); - }); + it('with TYPE', () => { + assert.deepEqual( + parseArgs(SCAN, '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( + parseArgs(SCAN, '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..2d6e4c35258 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -1,34 +1,63 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 parseScanArguments( + parser: CommandParser, + cursor: RedisArgument, + options?: ScanOptions +) { + parser.push(cursor); + if (options?.MATCH) { + parser.push('MATCH', options.MATCH); + } - if (options?.TYPE) { - args.push('TYPE', options.TYPE); - } - - return args; + if (options?.COUNT) { + parser.push('COUNT', options.COUNT.toString()); + } } -type ScanRawReply = [string, Array]; +export function pushScanArguments( + args: CommandArguments, + cursor: RedisArgument, + options?: ScanOptions +): CommandArguments { + args.push(cursor.toString()); -export interface ScanReply { - cursor: number; - keys: Array; + if (options?.MATCH) { + args.push('MATCH', options.MATCH); + } + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; } -export function transformReply([cursor, keys]: ScanRawReply): ScanReply { +export interface ScanOptions extends ScanCommonOptions { + TYPE?: RedisArgument; +} + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) { + parser.push('SCAN'); + parseScanArguments(parser, cursor, options); + + if (options?.TYPE) { + parser.push('TYPE', options.TYPE); + } + }, + 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..53434583832 100644 --- a/packages/client/lib/commands/SCARD.spec.ts +++ b/packages/client/lib/commands/SCARD.spec.ts @@ -1,19 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCARD'; +import { parseArgs } from './generic-transformers'; +import SCARD from './SCARD'; describe('SCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SCARD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SCARD, '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..61d4792d996 100644 --- a/packages/client/lib/commands/SCARD.ts +++ b/packages/client/lib/commands/SCARD.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export function transformArguments(key: string): Array { - return ['SCARD', key]; -} - -export declare function transformReply(): number; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 192f90f75a5..c98143a3415 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT DEBUG', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('NO'), - ['SCRIPT', 'DEBUG', 'NO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SCRIPT_DEBUG, '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..b0d3079068f 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts @@ -1,5 +1,11 @@ -export function transformArguments(mode: 'YES' | 'SYNC' | 'NO'): Array { - return ['SCRIPT', 'DEBUG', mode]; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 e0fbbcc5537..cf65156c72d 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT EXISTS', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('sha1'), - ['SCRIPT', 'EXISTS', 'sha1'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SCRIPT_EXISTS, 'sha1'), + ['SCRIPT', 'EXISTS', 'sha1'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SCRIPT', 'EXISTS', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SCRIPT_EXISTS, ['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..b0f6cbe2275 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts @@ -1,8 +1,13 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; -export function transformArguments(sha1: string | Array): RedisCommandArguments { - return pushVerdictArguments(['SCRIPT', 'EXISTS'], sha1); -} - -export { transformBooleanArrayReply as transformReply } from './generic-transformers'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 ae156e937d1..c51efd1a36c 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT FLUSH', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['SCRIPT', 'FLUSH'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SCRIPT_FLUSH), + ['SCRIPT', 'FLUSH'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('SYNC'), - ['SCRIPT', 'FLUSH', 'SYNC'] - ); - }); + it('with mode', () => { + assert.deepEqual( + parseArgs(SCRIPT_FLUSH, '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..1e05a619bad 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts @@ -1,11 +1,15 @@ -export function transformArguments(mode?: 'ASYNC' | 'SYNC'): Array { - const args = ['SCRIPT', 'FLUSH']; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, mode?: 'ASYNC' | 'SYNC') { + parser.push('SCRIPT', 'FLUSH'); if (mode) { - args.push(mode); + parser.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..7186efd54cf 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.spec.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.spec.ts @@ -1,11 +1,12 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SCRIPT_KILL'; +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( - transformArguments(), - ['SCRIPT', 'KILL'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SCRIPT_KILL), + ['SCRIPT', 'KILL'] + ); + }); }); diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts index c0a53da8681..26953506235 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['SCRIPT', 'KILL']; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 062f3c201e1..b0df9887e11 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts @@ -1,23 +1,24 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(SCRIPT_LOAD, 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..58f7c00dfcd 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.ts @@ -1,5 +1,11 @@ -export function transformArguments(script: string): Array { - return ['SCRIPT', 'LOAD', script]; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command, RedisArgument } from '../RESP/types'; -export declare function transformReply(): string; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 340906e9350..a943a80688d 100644 --- a/packages/client/lib/commands/SDIFF.spec.ts +++ b/packages/client/lib/commands/SDIFF.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SDIFF', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SDIFF', 'key'] - ); - }); + describe('processCommand', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SDIFF, 'key'), + ['SDIFF', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SDIFF', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SDIFF, ['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..bd78edc93db 100644 --- a/packages/client/lib/commands/SDIFF.ts +++ b/packages/client/lib/commands/SDIFF.ts @@ -1,14 +1,13 @@ -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 { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 263b4f43f64..43213adfbb0 100644 --- a/packages/client/lib/commands/SDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/SDIFFSTORE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SDIFFSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SDIFFSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SDIFFSTORE, 'destination', 'key'), + ['SDIFFSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SDIFFSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SDIFFSTORE, '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..6da2795d8ff 100644 --- a/packages/client/lib/commands/SDIFFSTORE.ts +++ b/packages/client/lib/commands/SDIFFSTORE.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + 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 0b3331fd3a4..b8aa57fe77b 100644 --- a/packages/client/lib/commands/SET.spec.ts +++ b/packages/client/lib/commands/SET.spec.ts @@ -1,129 +1,165 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SET', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['SET', 'key', 'value'] - ); - }); + describe('transformArguments', () => { + describe('value', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SET, 'key', 'value'), + ['SET', 'key', 'value'] + ); + }); + + it('number', () => { + assert.deepEqual( + parseArgs(SET, 'key', 0), + ['SET', 'key', '0'] + ); + }); + }); + + describe('expiration', () => { + it('\'KEEPTTL\'', () => { + assert.deepEqual( + parseArgs(SET, 'key', 'value', { + expiration: 'KEEPTTL' + }), + ['SET', 'key', 'value', 'KEEPTTL'] + ); + }); - it('number', () => { - assert.deepEqual( - transformArguments('key', 0), - ['SET', 'key', '0'] - ); - }); + it('{ type: \'KEEPTTL\' }', () => { + assert.deepEqual( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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( + parseArgs(SET, '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..d2d13c874c4 100644 --- a/packages/client/lib/commands/SET.ts +++ b/packages/client/lib/commands/SET.ts @@ -1,63 +1,87 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - const args = [ - 'SET', - key, - typeof value === 'number' ? value.toString() : value - ]; +export default { + 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?.EX !== undefined) { - args.push('EX', options.EX.toString()); + 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() + ); + } + } else if (options?.EX !== undefined) { + 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?.NX) { - args.push('NX'); + if (options?.condition) { + parser.push(options.condition); + } else if (options?.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; -} - -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..1eedcc69959 100644 --- a/packages/client/lib/commands/SETBIT.spec.ts +++ b/packages/client/lib/commands/SETBIT.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SETBIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['SETBIT', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SETBIT, '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..5cd29260071 100644 --- a/packages/client/lib/commands/SETBIT.ts +++ b/packages/client/lib/commands/SETBIT.ts @@ -1,14 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - offset: number, - value: BitValue -): RedisCommandArguments { - return ['SETBIT', key, offset.toString(), value.toString()]; -} - -export declare function transformReply(): BitValue; +export default { + IS_READ_ONLY: false, + 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 bca298c6c04..7bc934ccd68 100644 --- a/packages/client/lib/commands/SETEX.spec.ts +++ b/packages/client/lib/commands/SETEX.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SETEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'value'), - ['SETEX', 'key', '1', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SETEX, '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..5e58b589975 100644 --- a/packages/client/lib/commands/SETEX.ts +++ b/packages/client/lib/commands/SETEX.ts @@ -1,18 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - seconds: number, - value: RedisCommandArgument -): RedisCommandArguments { - return [ - 'SETEX', - key, - seconds.toString(), - value - ]; -} - -export declare function transformReply(): RedisCommandArgument; +export default { + 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 c5bdfcffa2c..81a5af3d411 100644 --- a/packages/client/lib/commands/SETNX .spec.ts +++ b/packages/client/lib/commands/SETNX .spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SETNX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['SETNX', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SETNX, '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..ae60067c28f 100644 --- a/packages/client/lib/commands/SETNX.ts +++ b/packages/client/lib/commands/SETNX.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { - return ['SETNX', key, value]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + 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 398b7730404..acdab5bcd3b 100644 --- a/packages/client/lib/commands/SETRANGE.spec.ts +++ b/packages/client/lib/commands/SETRANGE.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SETRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 'value'), - ['SETRANGE', 'key', '0', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SETRANGE, '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..42f4ca01117 100644 --- a/packages/client/lib/commands/SETRANGE.ts +++ b/packages/client/lib/commands/SETRANGE.ts @@ -1,13 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - offset: number, - value: RedisCommandArgument -): RedisCommandArguments { - return ['SETRANGE', key, offset.toString(), value]; -} - -export declare function transformReply(): number; +export default { + 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 d58cf4443c7..9c4ca852ad3 100644 --- a/packages/client/lib/commands/SHUTDOWN.spec.ts +++ b/packages/client/lib/commands/SHUTDOWN.spec.ts @@ -1,27 +1,50 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SHUTDOWN'; +import { strict as assert } from 'node:assert'; +import SHUTDOWN from './SHUTDOWN'; +import { parseArgs } from './generic-transformers'; describe('SHUTDOWN', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['SHUTDOWN'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SHUTDOWN), + ['SHUTDOWN'] + ); + }); + + it('with mode', () => { + assert.deepEqual( + parseArgs(SHUTDOWN, { + mode: 'NOSAVE' + }), + ['SHUTDOWN', 'NOSAVE'] + ); + }); - it('NOSAVE', () => { - assert.deepEqual( - transformArguments('NOSAVE'), - ['SHUTDOWN', 'NOSAVE'] - ); - }); + it('with NOW', () => { + assert.deepEqual( + parseArgs(SHUTDOWN, { + NOW: true + }), + ['SHUTDOWN', 'NOW'] + ); + }); + + it('with FORCE', () => { + assert.deepEqual( + parseArgs(SHUTDOWN, { + FORCE: true + }), + ['SHUTDOWN', 'FORCE'] + ); + }); - it('SAVE', () => { - assert.deepEqual( - transformArguments('SAVE'), - ['SHUTDOWN', 'SAVE'] - ); - }); + it('with ABORT', () => { + assert.deepEqual( + parseArgs(SHUTDOWN, { + ABORT: true + }), + ['SHUTDOWN', 'ABORT'] + ); }); + }); }); diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts index 1990d05a2ed..33fb3e77301 100644 --- a/packages/client/lib/commands/SHUTDOWN.ts +++ b/packages/client/lib/commands/SHUTDOWN.ts @@ -1,11 +1,34 @@ -export function transformArguments(mode?: 'NOSAVE' | 'SAVE'): Array { - const args = ['SHUTDOWN']; +import { CommandParser } from '../client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, options?: ShutdownOptions) { + parser.push('SHUTDOWN'); + + if (options?.mode) { + parser.push(options.mode); } - return args; -} + if (options?.NOW) { + parser.push('NOW'); + } + + if (options?.FORCE) { + parser.push('FORCE'); + } -export declare function transformReply(): void; + if (options?.ABORT) { + parser.push('ABORT'); + } + }, + 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..6ca7b959ca7 100644 --- a/packages/client/lib/commands/SINTER.spec.ts +++ b/packages/client/lib/commands/SINTER.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SINTER', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SINTER', 'key'] - ); - }); + describe('processCommand', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SINTER, 'key'), + ['SINTER', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SINTER', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SINTER, ['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..19ecdbb41ca 100644 --- a/packages/client/lib/commands/SINTER.ts +++ b/packages/client/lib/commands/SINTER.ts @@ -1,14 +1,13 @@ -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 { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 a93699f6a13..51aed13415d 100644 --- a/packages/client/lib/commands/SINTERCARD.spec.ts +++ b/packages/client/lib/commands/SINTERCARD.spec.ts @@ -1,30 +1,43 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(SINTERCARD, ['1', '2']), + ['SINTERCARD', '2', '1', '2'] + ); + }); + + it('with limit (backwards compatibility)', () => { + assert.deepEqual( + parseArgs(SINTERCARD, ['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( + parseArgs(SINTERCARD, ['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..cb9e7d3be3d 100644 --- a/packages/client/lib/commands/SINTERCARD.ts +++ b/packages/client/lib/commands/SINTERCARD.ts @@ -1,21 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: true, + // option `number` for backwards compatibility + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SInterCardOptions | number) { + parser.push('SINTERCARD'); + parser.pushKeysLength(keys); - if (limit) { - args.push('LIMIT', limit.toString()); + if (typeof options === 'number') { // backwards compatibility + parser.push('LIMIT', options.toString()); + } else if (options?.LIMIT !== undefined) { + parser.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..83302a5c829 100644 --- a/packages/client/lib/commands/SINTERSTORE.spec.ts +++ b/packages/client/lib/commands/SINTERSTORE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SINTERSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SINTERSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SINTERSTORE, 'destination', 'key'), + ['SINTERSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SINTERSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SINTERSTORE, '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..06db0af9cb0 100644 --- a/packages/client/lib/commands/SINTERSTORE.ts +++ b/packages/client/lib/commands/SINTERSTORE.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 8d18c83697a..4796475c52c 100644 --- a/packages/client/lib/commands/SISMEMBER.spec.ts +++ b/packages/client/lib/commands/SISMEMBER.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SISMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SISMEMBER', 'key', 'member'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(SISMEMBER, '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..6192ca2605f 100644 --- a/packages/client/lib/commands/SISMEMBER.ts +++ b/packages/client/lib/commands/SISMEMBER.ts @@ -1,12 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { - return ['SISMEMBER', key, member]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 b9c58c9eebb..6e2582e5abc 100644 --- a/packages/client/lib/commands/SMEMBERS.spec.ts +++ b/packages/client/lib/commands/SMEMBERS.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SMEMBERS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SMEMBERS', 'key'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(SMEMBERS, '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..6d018e999f4 100644 --- a/packages/client/lib/commands/SMEMBERS.ts +++ b/packages/client/lib/commands/SMEMBERS.ts @@ -1,9 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['SMEMBERS', key]; -} - -export declare function transformReply(): Array; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('SMEMBERS'); + parser.pushKey(key); + }, + 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..deff6912360 100644 --- a/packages/client/lib/commands/SMISMEMBER.spec.ts +++ b/packages/client/lib/commands/SMISMEMBER.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 './SMISMEMBER'; +import SMISMEMBER from './SMISMEMBER'; +import { parseArgs } from './generic-transformers'; describe('SMISMEMBER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SMISMEMBER', 'key', '1', '2'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(SMISMEMBER, '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..f0f3a143c7f 100644 --- a/packages/client/lib/commands/SMISMEMBER.ts +++ b/packages/client/lib/commands/SMISMEMBER.ts @@ -1,12 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - members: Array -): RedisCommandArguments { - return ['SMISMEMBER', key, ...members]; -} - -export { transformBooleanArrayReply as transformReply } from './generic-transformers'; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 e3308ee8143..c68a6e41914 100644 --- a/packages/client/lib/commands/SMOVE.spec.ts +++ b/packages/client/lib/commands/SMOVE.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SMOVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 'member'), - ['SMOVE', 'source', 'destination', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SMOVE, '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..d87eeefdfbf 100644 --- a/packages/client/lib/commands/SMOVE.ts +++ b/packages/client/lib/commands/SMOVE.ts @@ -1,13 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { - return ['SMOVE', source, destination, member]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 4967b020ad5..330b321a1b8 100644 --- a/packages/client/lib/commands/SORT.spec.ts +++ b/packages/client/lib/commands/SORT.spec.ts @@ -1,96 +1,100 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SORT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['SORT', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SORT, 'key'), + ['SORT', 'key'] + ); + }); - it('with BY', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern' - }), - ['SORT', 'key', 'BY', 'pattern'] - ); - }); + it('with BY', () => { + assert.deepEqual( + parseArgs(SORT, '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( + parseArgs(SORT, '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( + parseArgs(SORT, '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( + parseArgs(SORT, '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( + parseArgs(SORT, 'key', { + DIRECTION: 'ASC' + }), + ['SORT', 'key', 'ASC'] + ); + }); - it('with ALPHA', () => { - assert.deepEqual( - transformArguments('key', { - ALPHA: true - }), - ['SORT', 'key', 'ALPHA'] - ); - }); + it('with ALPHA', () => { + assert.deepEqual( + parseArgs(SORT, '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( + parseArgs(SORT, '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..3738d327d91 100644 --- a/packages/client/lib/commands/SORT.ts +++ b/packages/client/lib/commands/SORT.ts @@ -1,13 +1,60 @@ -import { RedisCommandArguments } from '.'; -import { pushSortArguments, SortOptions } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 parseSortArguments( + parser: CommandParser, + key: RedisArgument, + options?: SortOptions +) { + parser.pushKey(key); + + if (options?.BY) { + parser.push('BY', options.BY); + } + + if (options?.LIMIT) { + parser.push( + 'LIMIT', + options.LIMIT.offset.toString(), + options.LIMIT.count.toString() + ); + } + + if (options?.GET) { + if (Array.isArray(options.GET)) { + for (const pattern of options.GET) { + parser.push('GET', pattern); + } + } else { + parser.push('GET', options.GET); + } + } + + if (options?.DIRECTION) { + parser.push(options.DIRECTION); + } -export function transformArguments( - key: string, - options?: SortOptions -): RedisCommandArguments { - return pushSortArguments(['SORT', key], options); + if (options?.ALPHA) { + parser.push('ALPHA'); + } } -export declare function transformReply(): Array; +export default { + IS_READ_ONLY: true, + 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 fe3ca1240d7..86f8e507033 100644 --- a/packages/client/lib/commands/SORT_RO.spec.ts +++ b/packages/client/lib/commands/SORT_RO.spec.ts @@ -1,98 +1,102 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(SORT_RO, 'key'), + ['SORT_RO', 'key'] + ); + }); - it('with BY', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern' - }), - ['SORT_RO', 'key', 'BY', 'pattern'] - ); - }); + it('with BY', () => { + assert.deepEqual( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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( + parseArgs(SORT_RO, '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..9901907c223 100644 --- a/packages/client/lib/commands/SORT_RO.ts +++ b/packages/client/lib/commands/SORT_RO.ts @@ -1,15 +1,13 @@ -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, { parseSortArguments } from './SORT'; + +export default { + IS_READ_ONLY: true, + 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 d078135255d..a812cec52c5 100644 --- a/packages/client/lib/commands/SORT_STORE.spec.ts +++ b/packages/client/lib/commands/SORT_STORE.spec.ts @@ -1,96 +1,100 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SORT STORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['SORT', 'source', 'STORE', 'destination'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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( + parseArgs(SORT_STORE, '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..15c94732e41 100644 --- a/packages/client/lib/commands/SORT_STORE.ts +++ b/packages/client/lib/commands/SORT_STORE.ts @@ -1,17 +1,12 @@ -import { RedisCommandArguments } from '.'; -import { SortOptions } from './generic-transformers'; -import { transformArguments as transformSortArguments } from './SORT'; +import { CommandParser } from '../client/parser'; +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, - options?: SortOptions -): RedisCommandArguments { - const args = transformSortArguments(source, options); - args.push('STORE', destination); - return args; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + 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 6a384d181fc..f435134416b 100644 --- a/packages/client/lib/commands/SPOP.spec.ts +++ b/packages/client/lib/commands/SPOP.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 './SPOP'; +import SPOP from './SPOP'; +import { parseArgs } from './generic-transformers'; describe('SPOP', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['SPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SPOP, '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..38f40989e63 100644 --- a/packages/client/lib/commands/SPOP.ts +++ b/packages/client/lib/commands/SPOP.ts @@ -1,18 +1,11 @@ -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 { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: false, + 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 new file mode 100644 index 00000000000..935ff437800 --- /dev/null +++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts @@ -0,0 +1,23 @@ +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( + parseArgs(SPOP_COUNT, '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..0536203be97 --- /dev/null +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -0,0 +1,12 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: false, + 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 60b6ce2dad0..5a53bc40b7d 100644 --- a/packages/client/lib/commands/SPUBLISH.spec.ts +++ b/packages/client/lib/commands/SPUBLISH.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 './SPUBLISH'; +import SPUBLISH from './SPUBLISH'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(SPUBLISH, '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..77d93e617de 100644 --- a/packages/client/lib/commands/SPUBLISH.ts +++ b/packages/client/lib/commands/SPUBLISH.ts @@ -1,14 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const IS_READ_ONLY = true; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - channel: RedisCommandArgument, - message: RedisCommandArgument -): RedisCommandArguments { - return ['SPUBLISH', channel, message]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 291271540be..637aac27b29 100644 --- a/packages/client/lib/commands/SRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SRANDMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SRANDMEMBER', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SRANDMEMBER, '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..4285f7aa17c 100644 --- a/packages/client/lib/commands/SRANDMEMBER.ts +++ b/packages/client/lib/commands/SRANDMEMBER.ts @@ -1,9 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['SRANDMEMBER', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +export default { + IS_READ_ONLY: true, + 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 d3d787b3e63..13bb0d52d96 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SRANDMEMBER COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['SRANDMEMBER', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SRANDMEMBER_COUNT, '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..dd72245c3b3 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts @@ -1,16 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformSRandMemberArguments } from './SRANDMEMBER'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY, + 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 d53d7b0334d..6def4178fc8 100644 --- a/packages/client/lib/commands/SREM.spec.ts +++ b/packages/client/lib/commands/SREM.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SREM', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SREM', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SREM, 'key', 'member'), + ['SREM', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SREM', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SREM, '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..75053474cce 100644 --- a/packages/client/lib/commands/SREM.ts +++ b/packages/client/lib/commands/SREM.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 71a90bf81d8..e5d689c6e98 100644 --- a/packages/client/lib/commands/SSCAN.spec.ts +++ b/packages/client/lib/commands/SSCAN.spec.ts @@ -1,74 +1,56 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(SSCAN, '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( + parseArgs(SSCAN, '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( + parseArgs(SSCAN, '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( + parseArgs(SSCAN, '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..22634d56242 100644 --- a/packages/client/lib/commands/SSCAN.ts +++ b/packages/client/lib/commands/SSCAN.ts @@ -1,31 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, parseScanArguments} 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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + parser.push('SSCAN'); + parser.pushKey(key); + parseScanArguments(parser, 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..dbb7a08541b 100644 --- a/packages/client/lib/commands/STRLEN.spec.ts +++ b/packages/client/lib/commands/STRLEN.spec.ts @@ -1,26 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('STRLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['STRLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(STRLEN, '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..34e0430fc9e 100644 --- a/packages/client/lib/commands/STRLEN.ts +++ b/packages/client/lib/commands/STRLEN.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['STRLEN', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 2918607c1d6..a4389d4236e 100644 --- a/packages/client/lib/commands/SUNION.spec.ts +++ b/packages/client/lib/commands/SUNION.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SUNION', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SUNION', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SUNION, 'key'), + ['SUNION', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SUNION', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SUNION, ['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..3d9a5954a7c 100644 --- a/packages/client/lib/commands/SUNION.ts +++ b/packages/client/lib/commands/SUNION.ts @@ -1,14 +1,13 @@ -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 { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 142533eea2b..8f3db2cacd7 100644 --- a/packages/client/lib/commands/SUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/SUNIONSTORE.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SUNIONSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SUNIONSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SUNIONSTORE, 'destination', 'key'), + ['SUNIONSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SUNIONSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(SUNIONSTORE, '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..e2f43ecb1c8 100644 --- a/packages/client/lib/commands/SUNIONSTORE.ts +++ b/packages/client/lib/commands/SUNIONSTORE.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 add87512a64..a3b53b27218 100644 --- a/packages/client/lib/commands/SWAPDB.spec.ts +++ b/packages/client/lib/commands/SWAPDB.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('SWAPDB', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 1), - ['SWAPDB', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SWAPDB, 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..e59c75715cd 100644 --- a/packages/client/lib/commands/SWAPDB.ts +++ b/packages/client/lib/commands/SWAPDB.ts @@ -1,5 +1,12 @@ -export function transformArguments(index1: number, index2: number): Array { - return ['SWAPDB', index1.toString(), index2.toString()]; -} +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + 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; -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..4ee704f0dd0 100644 --- a/packages/client/lib/commands/TIME.spec.ts +++ b/packages/client/lib/commands/TIME.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 './TIME'; +import TIME from './TIME'; +import { parseArgs } from './generic-transformers'; describe('TIME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['TIME'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(TIME), + ['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..b25af710e1c 100644 --- a/packages/client/lib/commands/TIME.ts +++ b/packages/client/lib/commands/TIME.ts @@ -1,15 +1,14 @@ -export function transformArguments(): Array { - return ['TIME']; -} +import { CommandParser } from '../client/parser'; +import { BlobStringReply, Command } from '../RESP/types'; -interface TimeReply extends Date { - microseconds: number; -} - -export function transformReply(reply: [string, string]): TimeReply { - const seconds = Number(reply[0]), - microseconds = Number(reply[1]), - d: Partial = new Date(seconds * 1000 + microseconds / 1000); - d.microseconds = microseconds; - return d as TimeReply; -} +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..69a3498346b 100644 --- a/packages/client/lib/commands/TOUCH.spec.ts +++ b/packages/client/lib/commands/TOUCH.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('TOUCH', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['TOUCH', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(TOUCH, 'key'), + ['TOUCH', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['TOUCH', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(TOUCH, ['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..c765c9f8347 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 e37a6ab714b..4d36053c02e 100644 --- a/packages/client/lib/commands/TTL.spec.ts +++ b/packages/client/lib/commands/TTL.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('TTL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TTL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(TTL, '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..8420089fcb9 100644 --- a/packages/client/lib/commands/TTL.ts +++ b/packages/client/lib/commands/TTL.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['TTL', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 1040bf979b3..ae7392cdce9 100644 --- a/packages/client/lib/commands/TYPE.spec.ts +++ b/packages/client/lib/commands/TYPE.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('TYPE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TYPE', 'key'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(TYPE, '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..ffc592994db 100644 --- a/packages/client/lib/commands/TYPE.ts +++ b/packages/client/lib/commands/TYPE.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['TYPE', key]; -} - -export declare function transformReply(): RedisCommandArgument; +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 e8355407d8f..2c32bee8e33 100644 --- a/packages/client/lib/commands/UNLINK.spec.ts +++ b/packages/client/lib/commands/UNLINK.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('UNLINK', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['UNLINK', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(UNLINK, 'key'), + ['UNLINK', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['UNLINK', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(UNLINK, ['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..14d1e700277 100644 --- a/packages/client/lib/commands/UNLINK.ts +++ b/packages/client/lib/commands/UNLINK.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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/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..d2778e7967b 100644 --- a/packages/client/lib/commands/WAIT.spec.ts +++ b/packages/client/lib/commands/WAIT.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('WAIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 1), - ['WAIT', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(WAIT, 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..df45a12373d 100644 --- a/packages/client/lib/commands/WAIT.ts +++ b/packages/client/lib/commands/WAIT.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(numberOfReplicas: number, timeout: number): Array { - return ['WAIT', numberOfReplicas.toString(), timeout.toString()]; -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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/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..4ad60b256d0 100644 --- a/packages/client/lib/commands/XACK.spec.ts +++ b/packages/client/lib/commands/XACK.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XACK', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'group', '1-0'), - ['XACK', 'key', 'group', '1-0'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(XACK, '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( + parseArgs(XACK, '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..2500134f1c8 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 { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 4b556ecc27c..321581d0865 100644 --- a/packages/client/lib/commands/XADD.spec.ts +++ b/packages/client/lib/commands/XADD.spec.ts @@ -1,118 +1,94 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XADD, '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( + parseArgs(XADD, '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( + parseArgs(XADD, '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( + parseArgs(XADD, '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( + parseArgs(XADD, '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( + parseArgs(XADD, '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..cb9d0f5fad8 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -1,52 +1,57 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -interface XAddOptions { - NOMKSTREAM?: true; - TRIM?: { - strategy?: 'MAXLEN' | 'MINID'; - strategyModifier?: '=' | '~'; - threshold: number; - limit?: number; - }; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { Tail } from './generic-transformers'; + +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 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) { + parser.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); - } - - args.push(options.TRIM.threshold.toString()); - - if (options.TRIM.limit) { - args.push('LIMIT', options.TRIM.limit.toString()); - } + if (options.TRIM.strategyModifier) { + parser.push(options.TRIM.strategyModifier); } - args.push(id); + parser.push(options.TRIM.threshold.toString()); - for (const [key, value] of Object.entries(message)) { - args.push(key, value); + if (options.TRIM.limit) { + parser.push('LIMIT', options.TRIM.limit.toString()); } + } + + parser.push(id); - return args; + for (const [key, value] of Object.entries(message)) { + parser.push(key, value); + } } -export declare function transformReply(): string; +export default { + IS_READ_ONLY: false, + 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 new file mode 100644 index 00000000000..97927f212ff --- /dev/null +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts @@ -0,0 +1,96 @@ +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]); + + describe('transformArguments', () => { + it('single field', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }), + ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value'] + ); + }); + + it('multiple fields', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + '1': 'I', + '2': 'II' + }), + ['XADD', 'key', 'NOMKSTREAM', '*', '1', 'I', '2', 'II'] + ); + }); + + it('with TRIM', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.strategy', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.strategyModifier', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategyModifier: '=', + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', '=', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.limit', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, '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..9d33374be4a --- /dev/null +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts @@ -0,0 +1,11 @@ +import { BlobStringReply, NullReply, Command } from '../RESP/types'; +import { Tail } from './generic-transformers'; +import { parseXAddArguments } from './XADD'; + +export default { + IS_READ_ONLY: false, + 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 bae914bda05..58b09a63e78 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.spec.ts @@ -1,98 +1,69 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XAUTOCLAIM, '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( + parseArgs(XAUTOCLAIM, '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..19b4f63a2df 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.ts @@ -1,39 +1,41 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { StreamMessagesNullReply, transformStreamMessagesNullReply } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument, minIdleTime: number, - start: string, + start: RedisArgument, options?: XAutoClaimOptions -): RedisCommandArguments { - 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; -} - -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..78911657086 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts @@ -1,31 +1,38 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XAUTOCLAIM_JUSTID, '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..c0ebe83748e 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -1,25 +1,24 @@ -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 { + IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; + XAUTOCLAIM.parseCommand(...args); + parser.push('JUSTID'); + }, + 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..90768509225 100644 --- a/packages/client/lib/commands/XCLAIM.spec.ts +++ b/packages/client/lib/commands/XCLAIM.spec.ts @@ -1,120 +1,126 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { + IDLE: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1'] + ); + }); + + describe('with TIME', () => { + it('number', () => { + assert.deepEqual( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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( + parseArgs(XCLAIM, '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..598b1b17ba4 100644 --- a/packages/client/lib/commands/XCLAIM.ts +++ b/packages/client/lib/commands/XCLAIM.ts @@ -1,48 +1,59 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + 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 - ); - - if (options?.IDLE) { - args.push('IDLE', options.IDLE.toString()); + ) { + parser.push('XCLAIM'); + parser.pushKey(key); + parser.push(group, consumer, minIdleTime.toString()); + parser.pushVariadic(id); + + if (options?.IDLE !== undefined) { + parser.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) { + parser.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) { + parser.push('RETRYCOUNT', options.RETRYCOUNT.toString()); } if (options?.FORCE) { - args.push('FORCE'); + parser.push('FORCE'); } - return args; -} - -export { transformStreamMessagesNullReply as transformReply } from './generic-transformers'; + if (options?.LASTID !== undefined) { + parser.push('LASTID', options.LASTID); + } + }, + 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..d7bf9fdc70c 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts @@ -1,23 +1,25 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XCLAIM_JUSTID, '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..91be5aafbb4 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts @@ -1,13 +1,12 @@ -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 { + IS_READ_ONLY: XCLAIM.IS_READ_ONLY, + 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 00f9e2f9c67..510168bb765 100644 --- a/packages/client/lib/commands/XDEL.spec.ts +++ b/packages/client/lib/commands/XDEL.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', '0-0'), - ['XDEL', 'key', '0-0'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(XDEL, '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( + parseArgs(XDEL, '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..ee385203ce5 100644 --- a/packages/client/lib/commands/XDEL.ts +++ b/packages/client/lib/commands/XDEL.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + 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 57516e44cc8..7c9d6298c6b 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.spec.ts @@ -1,32 +1,45 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XGROUP CREATE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'group', '$'), - ['XGROUP', 'CREATE', 'key', 'group', '$'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(XGROUP_CREATE, 'key', 'group', '$'), + ['XGROUP', 'CREATE', 'key', 'group', '$'] + ); + }); + + it('with MKSTREAM', () => { + assert.deepEqual( + parseArgs(XGROUP_CREATE, '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( + parseArgs(XGROUP_CREATE, '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..e91186efe29 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.ts @@ -1,24 +1,35 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + id: RedisArgument, options?: XGroupCreateOptions -): RedisCommandArguments { - 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'); } - return args; -} + if (options?.ENTRIESREAD) { + parser.push('ENTRIESREAD', options.ENTRIESREAD.toString()); + } + }, + 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..eb749073d35 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts @@ -1,25 +1,29 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XGROUP_CREATECONSUMER, '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..906bc4c683e 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command, NumberReply } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument -): RedisCommandArguments { - return ['XGROUP', 'CREATECONSUMER', key, group, consumer]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument + ) { + 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 d071aedf64f..fabef789d78 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts @@ -1,23 +1,27 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XGROUP DELCONSUMER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', 'consumer'), - ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XGROUP_DELCONSUMER, '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..360d7e06cae 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument -): RedisCommandArguments { - return ['XGROUP', 'DELCONSUMER', key, group, consumer]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument + ) { + 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 ea8e7b7be98..8277c66d3f6 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts @@ -1,23 +1,27 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XGROUP DESTROY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XGROUP', 'DESTROY', 'key', 'group'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XGROUP_DESTROY, '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..9112f1bcd79 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument -): RedisCommandArguments { - return ['XGROUP', 'DESTROY', key, group]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; +export default { + IS_READ_ONLY: false, + 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 8df51f5401d..6ea0dd79c37 100644 --- a/packages/client/lib/commands/XGROUP_SETID.spec.ts +++ b/packages/client/lib/commands/XGROUP_SETID.spec.ts @@ -1,23 +1,27 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XGROUP SETID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', '0'), - ['XGROUP', 'SETID', 'key', 'group', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XGROUP_SETID, '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..5b0ddcc32ad 100644 --- a/packages/client/lib/commands/XGROUP_SETID.ts +++ b/packages/client/lib/commands/XGROUP_SETID.ts @@ -1,13 +1,27 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + id: RedisArgument, + options?: XGroupSetIdOptions + ) { + parser.push('XGROUP', 'SETID'); + parser.pushKey(key); + parser.push(group, id); + + if (options?.ENTRIESREAD) { + parser.push('ENTRIESREAD', options.ENTRIESREAD.toString()); + } + }, + 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..b1f245dbf18 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts @@ -1,43 +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_CONSUMERS'; +import XINFO_CONSUMERS from './XINFO_CONSUMERS'; +import { parseArgs } from './generic-transformers'; describe('XINFO CONSUMERS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XINFO', 'CONSUMERS', 'key', 'group'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XINFO_CONSUMERS, '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..310a40d17f3 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts @@ -1,28 +1,33 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - 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] - })); -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { + parser.push('XINFO', 'CONSUMERS'); + parser.pushKey(key); + parser.push(group); + }, + 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..a1196f4957a 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.spec.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.spec.ts @@ -1,48 +1,37 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XINFO GROUPS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['XINFO', 'GROUPS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XINFO_GROUPS, '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..e7f8874125a 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.ts @@ -1,25 +1,37 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - 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] - })); -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('XINFO', 'GROUPS'); + parser.pushKey(key); + }, + 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..7e1829f3059 100644 --- a/packages/client/lib/commands/XINFO_STREAM.spec.ts +++ b/packages/client/lib/commands/XINFO_STREAM.spec.ts @@ -1,72 +1,40 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(XINFO_STREAM, '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..bb102c591bb 100644 --- a/packages/client/lib/commands/XINFO_STREAM.ts +++ b/packages/client/lib/commands/XINFO_STREAM.ts @@ -1,64 +1,83 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { StreamMessageReply, transformTuplesReply } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('XINFO', 'STREAM'); + parser.pushKey(key); + }, + transformReply: { + // TODO: is there a "type safe" way to do it? + 2(reply: any) { + const parsedReply: Partial = {}; -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - 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 = {}; - - 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..3e22b9aebfa 100644 --- a/packages/client/lib/commands/XLEN.spec.ts +++ b/packages/client/lib/commands/XLEN.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['XLEN', 'key'] - ); - }); + it('processCommand', () => { + assert.deepEqual( + parseArgs(XLEN, '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..39d47187b28 100644 --- a/packages/client/lib/commands/XLEN.ts +++ b/packages/client/lib/commands/XLEN.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['XLEN', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 b1fef2a217f..55cb957fc62 100644 --- a/packages/client/lib/commands/XPENDING.spec.ts +++ b/packages/client/lib/commands/XPENDING.spec.ts @@ -1,62 +1,61 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XPENDING', () => { - describe('transformArguments', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XPENDING', 'key', 'group'] - ); - }); + describe('transformArguments', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(XPENDING, '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..11c944c61e7 100644 --- a/packages/client/lib/commands/XPENDING.ts +++ b/packages/client/lib/commands/XPENDING.ts @@ -1,44 +1,37 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - 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 { +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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; 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..33cd836f2a9 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.spec.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.spec.ts @@ -1,53 +1,67 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XPENDING RANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'group', '-', '+', 1), - ['XPENDING', 'key', 'group', '-', '+', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(XPENDING_RANGE, '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( + parseArgs(XPENDING_RANGE, '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( + parseArgs(XPENDING_RANGE, '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( + parseArgs(XPENDING_RANGE, '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..8d98ffe7f1e 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.ts @@ -1,56 +1,53 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + start: RedisArgument, + end: RedisArgument, count: number, options?: XPendingRangeOptions -): RedisCommandArguments { - const args = ['XPENDING', key, group]; + ) { + parser.push('XPENDING'); + parser.pushKey(key); + parser.push(group); - if (options?.IDLE) { - args.push('IDLE', options.IDLE.toString()); + if (options?.IDLE !== undefined) { + 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; -} - -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..b111a97aff1 100644 --- a/packages/client/lib/commands/XRANGE.spec.ts +++ b/packages/client/lib/commands/XRANGE.spec.ts @@ -1,30 +1,46 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['XRANGE', 'key', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(XRANGE, 'key', '-', '+'), + ['XRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + parseArgs(XRANGE, '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..de6bb6c9b1b 100644 --- a/packages/client/lib/commands/XRANGE.ts +++ b/packages/client/lib/commands/XRANGE.ts @@ -1,26 +1,38 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 xRangeArguments( + start: RedisArgument, + end: RedisArgument, + options?: XRangeOptions +) { + const args = [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 { + CACHEABLE: true, + IS_READ_ONLY: true, + 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, + 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..bb72c96497e 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 testUtils, { GLOBAL } from '../test-utils'; -import { FIRST_KEY_INDEX, transformArguments } from './XREAD'; +import { strict as assert } from 'node:assert'; +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( - FIRST_KEY_INDEX({ key: 'key', id: '' }), - 'key' - ); - }); + describe('FIRST_KEY_INDEX', () => { + it('single stream', () => { + assert.equal( + parseFirstKey(XREAD, { + key: 'key', + id: '' + }), + 'key' + ); + }); + + it('multiple streams', () => { + assert.equal( + parseFirstKey(XREAD, [{ + key: '1', + id: '' + }, { + key: '2', + id: '' + }]), + '1' + ); + }); + }); + + describe('transformArguments', () => { + it('single stream', () => { + assert.deepEqual( + parseArgs(XREAD, { + key: 'key', + id: '0-0' + }), + ['XREAD', 'STREAMS', 'key', '0-0'] + ); + }); - it('multiple streams', () => { - assert.equal( - FIRST_KEY_INDEX([{ key: '1', id: '' }, { key: '2', id: '' }]), - '1' - ); - }); + it('multiple streams', () => { + assert.deepEqual( + parseArgs(XREAD, [{ + key: '1', + id: '0-0' + }, { + key: '2', + id: '0-0' + }]), + ['XREAD', 'STREAMS', '1', '2', '0-0', '0-0'] + ); }); - describe('transformArguments', () => { - it('single stream', () => { - assert.deepEqual( - transformArguments({ - key: 'key', - id: '0' - }), - ['XREAD', 'STREAMS', 'key', '0'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + parseArgs(XREAD, { + key: 'key', + id: '0-0' + }, { + COUNT: 1 + }), + ['XREAD', 'COUNT', '1', '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('with BLOCK', () => { + assert.deepEqual( + parseArgs(XREAD, { + key: 'key', + id: '0-0' + }, { + BLOCK: 0 + }), + ['XREAD', '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 COUNT, BLOCK', () => { + assert.deepEqual( + parseArgs(XREAD, { + key: 'key', + id: '0-0' + }, { + COUNT: 1, + BLOCK: 0 + }), + ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '0-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..b57fb8f3983 100644 --- a/packages/client/lib/commands/XREAD.ts +++ b/packages/client/lib/commands/XREAD.ts @@ -1,46 +1,53 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 type XReadStreams = Array | XReadStream; -export const IS_READ_ONLY = true; +export function pushXReadStreams(parser: CommandParser, streams: XReadStreams) { + parser.push('STREAMS'); -interface XReadStream { - key: RedisCommandArgument; - id: RedisCommandArgument; + if (Array.isArray(streams)) { + for (let i = 0; i < streams.length; i++) { + parser.pushKey(streams[i].key); + } + for (let i = 0; i < streams.length; i++) { + parser.push(streams[i].id); + } + } else { + parser.pushKey(streams.key); + parser.push(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 { + IS_READ_ONLY: true, + 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 (typeof options?.BLOCK === 'number') { - args.push('BLOCK', options.BLOCK.toString()); + if (options?.BLOCK !== undefined) { + parser.push('BLOCK', options.BLOCK.toString()); } - args.push('STREAMS'); - - const streamsArray = Array.isArray(streams) ? streams : [streams], - argsLength = args.length; - for (let i = 0; i < streamsArray.length; i++) { - const stream = streamsArray[i]; - args[argsLength + i] = stream.key; - args[argsLength + streamsArray.length + i] = stream.id; - } - - return args; -} - -export { transformStreamsMessagesReply as transformReply } from './generic-transformers'; + pushXReadStreams(parser, streams); + }, + transformReply: { + 2: transformStreamsMessagesReplyResp2, + 3: undefined as unknown as () => ReplyUnion + }, + 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 fa196d504ad..085a67bc9b3 100644 --- a/packages/client/lib/commands/XREADGROUP.spec.ts +++ b/packages/client/lib/commands/XREADGROUP.spec.ts @@ -1,153 +1,158 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { FIRST_KEY_INDEX, transformArguments } from './XREADGROUP'; +import { strict as assert } from 'node:assert'; +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( - FIRST_KEY_INDEX('', '', { key: 'key', id: '' }), - 'key' - ); - }); + describe('FIRST_KEY_INDEX', () => { + it('single stream', () => { + assert.equal( + parseFirstKey(XREADGROUP, '', '', { key: 'key', id: '' }), + 'key' + ); + }); - it('multiple streams', () => { - assert.equal( - FIRST_KEY_INDEX('', '', [{ key: '1', id: '' }, { key: '2', id: '' }]), - '1' - ); - }); + it('multiple streams', () => { + assert.equal( + parseFirstKey(XREADGROUP, '', '', [{ 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( + parseArgs(XREADGROUP, '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( + parseArgs(XREADGROUP, '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( + parseArgs(XREADGROUP, '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( + parseArgs(XREADGROUP, '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( + parseArgs(XREADGROUP, '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( + parseArgs(XREADGROUP, '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..d0947e19a08 100644 --- a/packages/client/lib/commands/XREADGROUP.ts +++ b/packages/client/lib/commands/XREADGROUP.ts @@ -1,57 +1,42 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export interface XReadGroupStream { - key: RedisCommandArgument; - id: RedisCommandArgument; -} +import { CommandParser } from '../client/parser'; +import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; +import { XReadStreams, pushXReadStreams } from './XREAD'; +import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; 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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + group: RedisArgument, + consumer: RedisArgument, + streams: XReadStreams, options?: XReadGroupOptions -): RedisCommandArguments { - const args = ['XREADGROUP', 'GROUP', group, consumer]; + ) { + parser.push('XREADGROUP', 'GROUP', group, consumer); - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); } - if (typeof options?.BLOCK === 'number') { - args.push('BLOCK', options.BLOCK.toString()); + if (options?.BLOCK !== undefined) { + parser.push('BLOCK', options.BLOCK.toString()); } if (options?.NOACK) { - args.push('NOACK'); + parser.push('NOACK'); } - args.push('STREAMS'); - - const streamsArray = Array.isArray(streams) ? streams : [streams], - argsLength = args.length; - for (let i = 0; i < streamsArray.length; i++) { - const stream = streamsArray[i]; - args[argsLength + i] = stream.key; - args[argsLength + streamsArray.length + i] = stream.id; - } - - return args; -} - -export { transformStreamsMessagesReply as transformReply } from './generic-transformers'; + pushXReadStreams(parser, streams); + }, + 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..9872dc5e9e0 100644 --- a/packages/client/lib/commands/XREVRANGE.spec.ts +++ b/packages/client/lib/commands/XREVRANGE.spec.ts @@ -1,30 +1,46 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XREVRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['XREVRANGE', 'key', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(XREVRANGE, 'key', '-', '+'), + ['XREVRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + parseArgs(XREVRANGE, '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..ddc51082a1e 100644 --- a/packages/client/lib/commands/XREVRANGE.ts +++ b/packages/client/lib/commands/XREVRANGE.ts @@ -1,26 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { Command, RedisArgument } from '../RESP/types'; +import XRANGE, { xRangeArguments } 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 { + CACHEABLE: XRANGE.CACHEABLE, + IS_READ_ONLY: XRANGE.IS_READ_ONLY, + 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 e69de29bb2d..b3609695345 100644 --- a/packages/client/lib/commands/XSETID.spec.ts +++ b/packages/client/lib/commands/XSETID.spec.ts @@ -0,0 +1,47 @@ +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( + parseArgs(XSETID, 'key', '0-0'), + ['XSETID', 'key', '0-0'] + ); + }); + + it('with ENTRIESADDED', () => { + assert.deepEqual( + parseArgs(XSETID, 'key', '0-0', { + ENTRIESADDED: 1 + }), + ['XSETID', 'key', '0-0', 'ENTRIESADDED', '1'] + ); + }); + + it('with MAXDELETEDID', () => { + assert.deepEqual( + parseArgs(XSETID, '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..c76ac0b23a4 100644 --- a/packages/client/lib/commands/XSETID.ts +++ b/packages/client/lib/commands/XSETID.ts @@ -1,28 +1,32 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -interface XSetIdOptions { - ENTRIESADDED?: number; - MAXDELETEDID?: RedisCommandArgument; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + lastId: RedisArgument, options?: XSetIdOptions -): RedisCommandArguments { - 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); } + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; - return args; -} - -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..2c31f0fef92 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -1,49 +1,53 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('XTRIM', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'MAXLEN', 1), - ['XTRIM', 'key', 'MAXLEN', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(XTRIM, '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( + parseArgs(XTRIM, '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( + parseArgs(XTRIM, '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( + parseArgs(XTRIM, '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..fb617d8d35a 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,31 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, strategy: 'MAXLEN' | 'MINID', threshold: number, options?: XTrimOptions -): RedisCommandArguments { - 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; -} - -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..0e770693e3a 100644 --- a/packages/client/lib/commands/ZADD.spec.ts +++ b/packages/client/lib/commands/ZADD.spec.ts @@ -1,127 +1,146 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZADD, 'key', { + value: '1', + score: 1 + }), + ['ZADD', 'key', '1', '1'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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( + parseArgs(ZADD, '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..5ae71a151ba 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 { CommandParser } from '../client/parser'; +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 { + parseCommand( + parser: CommandParser, + 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'); - } + ) { + parser.push('ZADD'); + parser.pushKey(key); + + if (options?.condition) { + parser.push(options.condition); + } else if (options?.NX) { + parser.push('NX'); + } else if (options?.XX) { + parser.push('XX'); + } + + if (options?.comparison) { + parser.push(options.comparison); + } else if (options?.LT) { + parser.push('LT'); + } else if (options?.GT) { + parser.push('GT'); } - if ((options)?.CH) { - args.push('CH'); + if (options?.CH) { + parser.push('CH'); } - if ((options)?.INCR) { - args.push('INCR'); + pushMembers(parser, members); + }, + transformReply: transformDoubleReply +} as const satisfies Command; + +export function pushMembers( + parser: CommandParser, + members: SortedSetMember | Array) { + if (Array.isArray(members)) { + for (const member of members) { + pushMember(parser, member); } - - for (const { score, value } of (Array.isArray(members) ? members : [members])) { - args.push( - transformNumberInfinityArgument(score), - value - ); - } - - return args; + } else { + pushMember(parser, members); + } } -export { transformNumberInfinityReply as transformReply } from './generic-transformers'; +function pushMember( + parser: CommandParser, + member: SortedSetMember +) { + 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 new file mode 100644 index 00000000000..df9ac87f449 --- /dev/null +++ b/packages/client/lib/commands/ZADD_INCR.spec.ts @@ -0,0 +1,94 @@ +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( + parseArgs(ZADD_INCR, 'key', { + value: '1', + score: 1 + }), + ['ZADD', 'key', 'INCR', '1', '1'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + parseArgs(ZADD_INCR, 'key', [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]), + ['ZADD', 'key', 'INCR', '1', '1', '2', '2'] + ); + }); + + it('with condition', () => { + assert.deepEqual( + parseArgs(ZADD_INCR, 'key', { + value: '1', + score: 1 + }, { + condition: 'NX' + }), + ['ZADD', 'key', 'NX', 'INCR', '1', '1'] + ); + }); + + it('with comparison', () => { + assert.deepEqual( + parseArgs(ZADD_INCR, 'key', { + value: '1', + score: 1 + }, { + comparison: 'LT' + }), + ['ZADD', 'key', 'LT', 'INCR', '1', '1'] + ); + }); + + it('with CH', () => { + assert.deepEqual( + parseArgs(ZADD_INCR, 'key', { + value: '1', + score: 1 + }, { + CH: true + }), + ['ZADD', 'key', 'CH', 'INCR', '1', '1'] + ); + }); + + it('with condition, comparison, CH', () => { + assert.deepEqual( + parseArgs(ZADD_INCR, '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..f37554b1681 --- /dev/null +++ b/packages/client/lib/commands/ZADD_INCR.ts @@ -0,0 +1,39 @@ +import { CommandParser } from '../client/parser'; +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 { + parseCommand( + parser: CommandParser, + key: RedisArgument, + members: SortedSetMember | Array, + options?: ZAddOptions + ) { + parser.push('ZADD'); + parser.pushKey(key); + + if (options?.condition) { + parser.push(options.condition); + } + + if (options?.comparison) { + parser.push(options.comparison); + } + + if (options?.CH) { + parser.push('CH'); + } + + parser.push('INCR'); + + 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 2e90da772b6..44adec0833a 100644 --- a/packages/client/lib/commands/ZCARD.spec.ts +++ b/packages/client/lib/commands/ZCARD.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZCARD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZCARD, '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..57b9e7f1d47 100644 --- a/packages/client/lib/commands/ZCARD.ts +++ b/packages/client/lib/commands/ZCARD.ts @@ -1,11 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['ZCARD', key]; -} - -export declare function transformReply(): number; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 e185ed3cd45..5d279d7a4ca 100644 --- a/packages/client/lib/commands/ZCOUNT.spec.ts +++ b/packages/client/lib/commands/ZCOUNT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZCOUNT', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZCOUNT, '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..ccbc3d13d9b 100644 --- a/packages/client/lib/commands/ZCOUNT.ts +++ b/packages/client/lib/commands/ZCOUNT.ts @@ -1,21 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 { - return [ - 'ZCOUNT', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; -} - -export declare function transformReply(): number; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: number | RedisArgument, + max: number | RedisArgument + ) { + 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 8bb1a101f53..4914df3e978 100644 --- a/packages/client/lib/commands/ZDIFF.spec.ts +++ b/packages/client/lib/commands/ZDIFF.spec.ts @@ -1,30 +1,34 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZDIFF, 'key'), + ['ZDIFF', '1', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZDIFF', '2', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ZDIFF, ['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..28135dc9c13 100644 --- a/packages/client/lib/commands/ZDIFF.ts +++ b/packages/client/lib/commands/ZDIFF.ts @@ -1,14 +1,12 @@ -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 { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + 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 c63902b2666..7f380cfc532 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.spec.ts @@ -1,30 +1,34 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZDIFFSTORE, 'destination', 'key'), + ['ZDIFFSTORE', 'destination', '1', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['ZDIFFSTORE', 'destination', '2', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ZDIFFSTORE, '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..d83a4bdc851 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: true, + 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 3b9cb725aaa..bea639f223e 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts @@ -1,30 +1,34 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZDIFF_WITHSCORES, 'key'), + ['ZDIFF', '1', 'key', 'WITHSCORES'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZDIFF', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ZDIFF_WITHSCORES, ['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..4088f106dc6 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 { CommandParser } from '../client/parser'; +import { Command } from '../RESP/types'; +import { RedisVariadicArgument, transformSortedSetReply } from './generic-transformers'; +import ZDIFF from './ZDIFF'; -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 { + IS_READ_ONLY: ZDIFF.IS_READ_ONLY, + 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 bf2a34b0965..8f6c5141252 100644 --- a/packages/client/lib/commands/ZINCRBY.spec.ts +++ b/packages/client/lib/commands/ZINCRBY.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'member'), - ['ZINCRBY', 'key', '1', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZINCRBY, '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..5e461891e9c 100644 --- a/packages/client/lib/commands/ZINCRBY.ts +++ b/packages/client/lib/commands/ZINCRBY.ts @@ -1,19 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformNumberInfinityArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 { + parseCommand( + parser: CommandParser, + key: RedisArgument, increment: number, - member: RedisCommandArgument -): RedisCommandArguments { - return [ - 'ZINCRBY', - key, - transformNumberInfinityArgument(increment), - member - ]; -} - -export { transformNumberInfinityReply as transformReply } from './generic-transformers'; + member: RedisArgument + ) { + 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 4d2d86c8869..73df0935de9 100644 --- a/packages/client/lib/commands/ZINTER.spec.ts +++ b/packages/client/lib/commands/ZINTER.spec.ts @@ -1,58 +1,66 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZINTER, 'key'), + ['ZINTER', '1', 'key'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZINTER', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + parseArgs(ZINTER, ['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( + parseArgs(ZINTER, { + 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( + parseArgs(ZINTER, [{ + 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( + parseArgs(ZINTER, '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..740d3c295ec 100644 --- a/packages/client/lib/commands/ZINTER.ts +++ b/packages/client/lib/commands/ZINTER.ts @@ -1,33 +1,37 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ZKeys, parseZKeysArguments } 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 function transformArguments( - keys: Array | RedisCommandArgument, - options?: ZInterOptions -): RedisCommandArguments { - const args = pushVerdictArgument(['ZINTER'], keys); +export type ZInterKeysType = ZInterKeys | ZInterKeys; - if (options?.WEIGHTS) { - args.push( - 'WEIGHTS', - ...options.WEIGHTS.map(weight => weight.toString()) - ); - } +export interface ZInterOptions { + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +} - if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); - } +export function parseZInterArguments( + parser: CommandParser, + keys: ZKeys, + options?: ZInterOptions +) { + parseZKeysArguments(parser, keys); - return args; + if (options?.AGGREGATE) { + parser.push('AGGREGATE', options.AGGREGATE); + } } -export declare function transformReply(): Array; +export default { + IS_READ_ONLY: true, + 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 492c1a90433..5204872a2d0 100644 --- a/packages/client/lib/commands/ZINTERCARD.spec.ts +++ b/packages/client/lib/commands/ZINTERCARD.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 './ZINTERCARD'; +import ZINTERCARD from './ZINTERCARD'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZINTERCARD, ['1', '2']), + ['ZINTERCARD', '2', '1', '2'] + ); }); - testUtils.testWithClient('client.zInterCard', async client => { + describe('with LIMIT', () => { + it('plain number (backwards compatibility)', () => { + assert.deepEqual( + parseArgs(ZINTERCARD, ['1', '2'], 1), + ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] + ); + }); + + it('{ LIMIT: number }', () => { assert.deepEqual( - await client.zInterCard('key'), - 0 + parseArgs(ZINTERCARD, ['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..8c2e98d12cb 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 { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + keys: RedisVariadicArgument, + options?: ZInterCardOptions['LIMIT'] | ZInterCardOptions + ) { + parser.push('ZINTERCARD'); + parser.pushKeysLength(keys); - if (limit) { - args.push('LIMIT', limit.toString()); + // backwards compatibility + if (typeof options === 'number') { + parser.push('LIMIT', options.toString()); + } else if (options?.LIMIT) { + parser.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..c6b448ab908 100644 --- a/packages/client/lib/commands/ZINTERSTORE.spec.ts +++ b/packages/client/lib/commands/ZINTERSTORE.spec.ts @@ -1,56 +1,64 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZINTERSTORE', () => { - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['ZINTERSTORE', 'destination', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + parseArgs(ZINTERSTORE, '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( + parseArgs(ZINTERSTORE, '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( + parseArgs(ZINTERSTORE, '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( + parseArgs(ZINTERSTORE, '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( + parseArgs(ZINTERSTORE, '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..dcbe153cfc7 100644 --- a/packages/client/lib/commands/ZINTERSTORE.ts +++ b/packages/client/lib/commands/ZINTERSTORE.ts @@ -1,32 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { ZKeys } from './generic-transformers'; +import { parseZInterArguments, ZInterOptions } from './ZINTER'; -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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + destination: RedisArgument, + keys: ZKeys, + options?: ZInterOptions + ) { + 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 0eaeb26a244..234b250b143 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts @@ -1,58 +1,66 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZINTER_WITHSCORES, 'key'), + ['ZINTER', '1', 'key', 'WITHSCORES'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZINTER', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + parseArgs(ZINTER_WITHSCORES, ['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( + parseArgs(ZINTER_WITHSCORES, { + 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( + parseArgs(ZINTER_WITHSCORES, [{ + 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( + parseArgs(ZINTER_WITHSCORES, '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..d3a6614b3c2 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts @@ -1,13 +1,13 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZInterArguments } from './ZINTER'; +import { Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; +import ZINTER from './ZINTER'; -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 { + IS_READ_ONLY: ZINTER.IS_READ_ONLY, + 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 85809f1a9a9..78c7411affd 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.spec.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZLEXCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '[a', '[b'), - ['ZLEXCOUNT', 'key', '[a', '[b'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZLEXCOUNT, '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..7536590c168 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.ts @@ -1,20 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { - return [ - 'ZLEXCOUNT', - key, - min, - max - ]; -} - -export declare function transformReply(): number; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: RedisArgument, + max: RedisArgument + ) { + 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 9a0c9676c20..c15a53b7313 100644 --- a/packages/client/lib/commands/ZMPOP.spec.ts +++ b/packages/client/lib/commands/ZMPOP.spec.ts @@ -1,32 +1,56 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZMPOP, '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( + parseArgs(ZMPOP, '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..0e47108e25f 100644 --- a/packages/client/lib/commands/ZMPOP.ts +++ b/packages/client/lib/commands/ZMPOP.ts @@ -1,34 +1,65 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { SortedSetSide, transformSortedSetMemberReply, transformZMPopArguments, ZMember, ZMPopOptions } 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 const FIRST_KEY_INDEX = 2; - -export function transformArguments( - keys: RedisCommandArgument | Array, - side: SortedSetSide, - options?: ZMPopOptions -): RedisCommandArguments { - return transformZMPopArguments( - ['ZMPOP'], - keys, - side, - options - ); +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 parseZMPopArguments( + parser: CommandParser, + keys: RedisVariadicArgument, + side: SortedSetSide, + options?: ZMPopOptions +) { + parser.pushKeysLength(keys); -export function transformReply(reply: ZMPopRawReply): ZMPopReply { - return reply === null ? null : { - key: reply[0], - elements: reply[1].map(transformSortedSetMemberReply) - }; + parser.push(side); + + if (options?.COUNT) { + parser.push('COUNT', options.COUNT.toString()); + } } + +export type ZMPopArguments = Tail>; + +export default { + IS_READ_ONLY: false, + 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) { + 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..6c6d2946e00 100644 --- a/packages/client/lib/commands/ZMSCORE.spec.ts +++ b/packages/client/lib/commands/ZMSCORE.spec.ts @@ -1,30 +1,34 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZMSCORE, 'key', 'member'), + ['ZMSCORE', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['ZMSCORE', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ZMSCORE, '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..b225b35dfd3 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 { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, NullReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { createTransformNullableDoubleReplyResp2Func, 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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { + parser.push('ZMSCORE'); + parser.pushKey(key); + parser.pushVariadic(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..1796647df86 100644 --- a/packages/client/lib/commands/ZPOPMAX.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX.spec.ts @@ -1,41 +1,40 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMAX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZPOPMAX', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZPOPMAX, '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..05c7f35e052 100644 --- a/packages/client/lib/commands/ZPOPMAX.ts +++ b/packages/client/lib/commands/ZPOPMAX.ts @@ -1,12 +1,29 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ZPOPMAX'); + parser.pushKey(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..dd9d85dbd36 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts @@ -1,19 +1,33 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMAX COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZPOPMAX', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZPOPMAX_COUNT, '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..888ce039fbe 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts @@ -1,16 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZPopMaxArguments } from './ZPOPMAX'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + 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 624b7054404..653a4e70a92 100644 --- a/packages/client/lib/commands/ZPOPMIN.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN.spec.ts @@ -1,41 +1,40 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMIN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZPOPMIN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZPOPMIN, '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..6295925aef1 100644 --- a/packages/client/lib/commands/ZPOPMIN.ts +++ b/packages/client/lib/commands/ZPOPMIN.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + 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 6d40002ab72..126a3cc1e9a 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts @@ -1,19 +1,33 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMIN COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZPOPMIN', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZPOPMIN_COUNT, '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..2b6abf580b9 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts @@ -1,16 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZPopMinArguments } from './ZPOPMIN'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + 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 c57d26f830e..a25ea79f8e1 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.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 './ZRANDMEMBER'; +import ZRANDMEMBER from './ZRANDMEMBER'; +import { parseArgs } from './generic-transformers'; describe('ZRANDMEMBER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZRANDMEMBER', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZRANDMEMBER, '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..2abd9d3684c 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return ['ZRANDMEMBER', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + 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 10db0727b23..eee0d454975 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.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 './ZRANDMEMBER_COUNT'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZRANDMEMBER_COUNT, '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..42ef8110639 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts @@ -1,16 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER'; +import { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY, + 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 5b5ec1f500f..3be3b92aeef 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.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 './ZRANDMEMBER_COUNT_WITHSCORES'; +import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZRANDMEMBER_COUNT_WITHSCORES, '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..f096e9d807d 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -1,13 +1,13 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZRandMemberCountArguments } from './ZRANDMEMBER_COUNT'; +import { CommandParser } from '../client/parser'; +import { Command, RedisArgument } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; -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 { + IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY, + 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 a280aff0033..a780e4ef613 100644 --- a/packages/client/lib/commands/ZRANGE.spec.ts +++ b/packages/client/lib/commands/ZRANGE.spec.ts @@ -1,74 +1,78 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGE', 'src', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ZRANGE, '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( + parseArgs(ZRANGE, '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( + parseArgs(ZRANGE, '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( + parseArgs(ZRANGE, '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( + parseArgs(ZRANGE, '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( + parseArgs(ZRANGE, '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..d1bc3433a50 100644 --- a/packages/client/lib/commands/ZRANGE.ts +++ b/packages/client/lib/commands/ZRANGE.ts @@ -1,51 +1,64 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export interface ZRangeOptions { + BY?: 'SCORE' | 'LEX'; + REV?: boolean; + LIMIT?: { + offset: number; + count: number; + }; +} -export const IS_READ_ONLY = true; +export function zRangeArgument( + min: RedisArgument | number, + max: RedisArgument | number, + options?: ZRangeOptions +) { + const args = [ + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) + ] -interface ZRangeOptions { - BY?: 'SCORE' | 'LEX'; - REV?: true; - LIMIT?: { - offset: number; - count: number; - }; -} + switch (options?.BY) { + case 'SCORE': + args.push('BYSCORE'); + break; -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number, - options?: ZRangeOptions -): RedisCommandArguments { - const args = [ - 'ZRANGE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(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; + 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 declare function transformReply(): Array; +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, + options?: ZRangeOptions + ) { + 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 fe7b7d5a16e..942e184661a 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts @@ -1,33 +1,37 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYLEX', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', '-', '+'), - ['ZRANGEBYLEX', 'src', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ZRANGEBYLEX, '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( + parseArgs(ZRANGEBYLEX, '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..316d9745c7e 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 { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: RedisArgument, + max: RedisArgument, options?: ZRangeByLexOptions -): RedisCommandArguments { - const args = [ - 'ZRANGEBYLEX', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; + ) { + 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; -} - -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..364882f21a9 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts @@ -1,33 +1,37 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYSCORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGEBYSCORE', 'src', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ZRANGEBYSCORE, '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( + parseArgs(ZRANGEBYSCORE, '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..4d5471fdc0b 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 { CommandParser } from '../client/parser'; +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 { + CACHEABLE: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, min: string | number, max: string | number, options?: ZRangeByScoreOptions -): RedisCommandArguments { - const args = [ - 'ZRANGEBYSCORE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; + ) { + 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; -} - -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..191eaa4e34f 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts @@ -1,33 +1,37 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYSCORE WITHSCORES', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ZRANGEBYSCORE_WITHSCORES, '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( + parseArgs(ZRANGEBYSCORE_WITHSCORES, '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..1a759b23dce 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts @@ -1,18 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ZRangeByScoreOptions, transformArguments as transformZRangeByScoreArguments } from './ZRANGEBYSCORE'; +import { Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; +import ZRANGEBYSCORE from './ZRANGEBYSCORE'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGEBYSCORE'; +export default { + CACHEABLE: ZRANGEBYSCORE.CACHEABLE, + IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; -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'; + 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 7af253e539f..c9708efd6fd 100644 --- a/packages/client/lib/commands/ZRANGESTORE.spec.ts +++ b/packages/client/lib/commands/ZRANGESTORE.spec.ts @@ -1,92 +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 './ZRANGESTORE'; +import ZRANGESTORE from './ZRANGESTORE'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZRANGESTORE, '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( + parseArgs(ZRANGESTORE, '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( + parseArgs(ZRANGESTORE, '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( + parseArgs(ZRANGESTORE, '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( + parseArgs(ZRANGESTORE, '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( + parseArgs(ZRANGESTORE, '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..f73e93a506f 100644 --- a/packages/client/lib/commands/ZRANGESTORE.ts +++ b/packages/client/lib/commands/ZRANGESTORE.ts @@ -1,62 +1,51 @@ -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 { CommandParser } from '../client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + destination: RedisArgument, + source: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, options?: ZRangeStoreOptions -): RedisCommandArguments { - const args = [ - 'ZRANGESTORE', - dst, - src, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; + ) { + parser.push('ZRANGESTORE'); + parser.pushKey(destination); + parser.pushKey(source); + parser.push( + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) + ); switch (options?.BY) { - case 'SCORE': - args.push('BYSCORE'); - break; + case 'SCORE': + parser.push('BYSCORE'); + break; - case 'LEX': - args.push('BYLEX'); - break; + case 'LEX': + 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()); - } - - if (options?.WITHSCORES) { - args.push('WITHSCORES'); - } - - return args; -} - -export function transformReply(reply: number): number { - if (typeof reply !== 'number') { - throw new TypeError(`Upgrade to Redis 6.2.5 and up (https://github.com/redis/redis/pull/9089)`); + parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } - - 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..e3009a6eadb 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts @@ -1,65 +1,76 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANGE WITHSCORES', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGE', 'src', '0', '1', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ZRANGE_WITHSCORES, '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( + parseArgs(ZRANGE_WITHSCORES, '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( + parseArgs(ZRANGE_WITHSCORES, '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( + parseArgs(ZRANGE_WITHSCORES, '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( + parseArgs(ZRANGE_WITHSCORES, '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..7e6cf00cf2e 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts @@ -1,13 +1,16 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZRangeArguments } from './ZRANGE'; +import { Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; +import ZRANGE from './ZRANGE'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGE'; +export default { + CACHEABLE: ZRANGE.CACHEABLE, + IS_READ_ONLY: ZRANGE.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZRangeArguments(...args), - 'WITHSCORES' - ]; -} + ZRANGE.parseCommand(...args); + parser.push('WITHSCORES'); + }, + transformReply: transformSortedSetReply +} as const satisfies Command; -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..480f75f66e1 100644 --- a/packages/client/lib/commands/ZRANK.spec.ts +++ b/packages/client/lib/commands/ZRANK.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZRANK', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZRANK, '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..045e9ef8c25 100644 --- a/packages/client/lib/commands/ZRANK.ts +++ b/packages/client/lib/commands/ZRANK.ts @@ -1,14 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { - return ['ZRANK', key, member]; -} - -export declare function transformReply(): number | null; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 new file mode 100644 index 00000000000..9fa7cb1f6fd --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts @@ -0,0 +1,47 @@ +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( + parseArgs(ZRANK_WITHSCORE, '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..dc2e48b362d --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -0,0 +1,31 @@ +import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, UnwrapReply, Command } from '../RESP/types'; +import ZRANK from './ZRANK'; + +export default { + CACHEABLE: ZRANK.CACHEABLE, + IS_READ_ONLY: ZRANK.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; + + ZRANK.parseCommand(...args); + parser.push('WITHSCORE'); + }, + 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..ac65b3d0139 100644 --- a/packages/client/lib/commands/ZREM.spec.ts +++ b/packages/client/lib/commands/ZREM.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZREM', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZREM', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(ZREM, 'key', 'member'), + ['ZREM', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['ZREM', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + parseArgs(ZREM, '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..c8ba0ec02a6 100644 --- a/packages/client/lib/commands/ZREM.ts +++ b/packages/client/lib/commands/ZREM.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + member: RedisVariadicArgument + ) { + 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 b59c9e9f3b0..b141b7679ee 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZREMRANGEBYLEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '[a', '[b'), - ['ZREMRANGEBYLEX', 'key', '[a', '[b'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZREMRANGEBYLEX, '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..5d7e1a21bb0 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts @@ -1,19 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 { - return [ - 'ZREMRANGEBYLEX', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number + ) { + 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 c659dadb790..19f54466c20 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZREMRANGEBYRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZREMRANGEBYRANK', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZREMRANGEBYRANK, '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..0a2eb3fadf3 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts @@ -1,13 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, start: number, stop: number -): RedisCommandArguments { - return ['ZREMRANGEBYRANK', key, start.toString(), stop.toString()]; -} - -export declare function transformReply(): 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 988fd7690c9..856692ef8f5 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts @@ -1,19 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREMRANGEBYSCORE'; +import { parseArgs } from './generic-transformers'; +import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; describe('ZREMRANGEBYSCORE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZREMRANGEBYSCORE', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZREMRANGEBYSCORE, '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..3d23d875948 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts @@ -1,19 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +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 { - return [ - 'ZREMRANGEBYSCORE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) - ]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, + ) { + 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 d9fef0d70a4..c89f528eb1c 100644 --- a/packages/client/lib/commands/ZREVRANK.spec.ts +++ b/packages/client/lib/commands/ZREVRANK.spec.ts @@ -1,19 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREVRANK'; +import { parseArgs } from './generic-transformers'; +import ZREVRANK from './ZREVRANK'; describe('ZREVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZREVRANK', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZREVRANK, '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..d48dc68adc2 100644 --- a/packages/client/lib/commands/ZREVRANK.ts +++ b/packages/client/lib/commands/ZREVRANK.ts @@ -1,14 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { - return ['ZREVRANK', key, member]; -} - -export declare function transformReply(): number | null; +import { CommandParser } from '../client/parser'; +import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 afa221a1ef3..f8064aea41e 100644 --- a/packages/client/lib/commands/ZSCAN.spec.ts +++ b/packages/client/lib/commands/ZSCAN.spec.ts @@ -1,77 +1,53 @@ -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 { parseArgs } from './generic-transformers'; +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( + parseArgs(ZSCAN, '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( + parseArgs(ZSCAN, '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( + parseArgs(ZSCAN, '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( + parseArgs(ZSCAN, '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..051235033eb 100644 --- a/packages/client/lib/commands/ZSCAN.ts +++ b/packages/client/lib/commands/ZSCAN.ts @@ -1,39 +1,29 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, transformNumberInfinityReply, pushScanArguments, ZMember } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, parseScanArguments } 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 { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + parser.push('ZSCAN'); + parser.pushKey(key); + parseScanArguments(parser, 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..4229ab7aac0 100644 --- a/packages/client/lib/commands/ZSCORE.spec.ts +++ b/packages/client/lib/commands/ZSCORE.spec.ts @@ -1,19 +1,23 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZSCORE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZSCORE', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ZSCORE, '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..23b52901078 100644 --- a/packages/client/lib/commands/ZSCORE.ts +++ b/packages/client/lib/commands/ZSCORE.ts @@ -1,14 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { - return ['ZSCORE', key, member]; -} - -export { transformNumberInfinityNullReply as transformReply } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformNullableDoubleReply } from './generic-transformers'; + +export default { + CACHEABLE: true, + IS_READ_ONLY: true, + 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 c53498cbf65..b4dbb4de603 100644 --- a/packages/client/lib/commands/ZUNION.spec.ts +++ b/packages/client/lib/commands/ZUNION.spec.ts @@ -1,48 +1,66 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZUNION, 'key'), + ['ZUNION', '1', 'key'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZUNION', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + parseArgs(ZUNION, ['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( + parseArgs(ZUNION, { + key: 'key', + weight: 1 + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('keys & weights', () => { + assert.deepEqual( + parseArgs(ZUNION, [{ + 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( + parseArgs(ZUNION, '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..a91dc68bc09 100644 --- a/packages/client/lib/commands/ZUNION.ts +++ b/packages/client/lib/commands/ZUNION.ts @@ -1,30 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ZKeys, parseZKeysArguments } 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, - options?: ZUnionOptions -): RedisCommandArguments { - const args = pushVerdictArgument(['ZUNION'], keys); - - if (options?.WEIGHTS) { - args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString())); - } +export default { + IS_READ_ONLY: true, + 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; -} - -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..a369a649311 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.spec.ts @@ -1,56 +1,64 @@ -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'; +import { parseArgs } from './generic-transformers'; describe('ZUNIONSTORE', () => { - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['ZUNIONSTORE', 'destination', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + parseArgs(ZUNIONSTORE, '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( + parseArgs(ZUNIONSTORE, '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( + parseArgs(ZUNIONSTORE, '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( + parseArgs(ZUNIONSTORE, '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( + parseArgs(ZUNIONSTORE, '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..c88f5a5a6f9 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.ts @@ -1,29 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command, } from '../RESP/types'; +import { ZKeys, parseZKeysArguments } 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + 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())); - } - + ): 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; -} - -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..dee735fc99f 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts @@ -1,48 +1,66 @@ -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'; +import { parseArgs } from './generic-transformers'; 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( + parseArgs(ZUNION_WITHSCORES, 'key'), + ['ZUNION', '1', 'key', 'WITHSCORES'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZUNION', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + parseArgs(ZUNION_WITHSCORES, ['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( + parseArgs(ZUNION_WITHSCORES, { + key: 'key', + weight: 1 + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] + ); + }); + + it('keys & weights', () => { + assert.deepEqual( + parseArgs(ZUNION_WITHSCORES, [{ + 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( + parseArgs(ZUNION_WITHSCORES, '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..c62df55518f 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts @@ -1,13 +1,15 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZUnionArguments } from './ZUNION'; +import { Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; +import ZUNION from './ZUNION'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZUNION'; -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZUnionArguments(...args), - 'WITHSCORES' - ]; -} +export default { + IS_READ_ONLY: ZUNION.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; + ZUNION.parseCommand(...args); + parser.push('WITHSCORES'); + }, + 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..91eab7107a1 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -1,697 +1,664 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +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, Command } 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 function transformNumberInfinityNullArrayReply(reply: Array): Array { - return reply.map(transformNumberInfinityNullReply); +export interface Stringable { + toString(): string; } -export function transformNumberInfinityArgument(num: number): string { - switch (num) { - case Infinity: - return '+inf'; +export function transformTuplesToMap( + reply: UnwrapReply>, + func: (elem: any) => T, +) { + const message = Object.create(null); - case -Infinity: - return '-inf'; + for (let i = 0; i < reply.length; i+= 2) { + message[reply[i].toString()] = func(reply[i + 1]); + } - default: - return num.toString(); - } + return message; } -export function transformStringNumberInfinityArgument(num: RedisCommandArgument | number): RedisCommandArgument { - if (typeof num !== 'number') return num; - - return transformNumberInfinityArgument(num); +export function createTransformTuplesReplyFunc(preserve?: any, typeMapping?: TypeMapping) { + return (reply: ArrayReply) => { + return transformTuplesReply(reply, preserve, typeMapping); + }; } -export function transformTuplesReply( - reply: Array -): Record { - const message = Object.create(null); - - for (let i = 0; i < reply.length; i += 2) { - message[reply[i].toString()] = reply[i + 1]; - } +export function transformTuplesReply( + reply: ArrayReply, + preserve?: any, + typeMapping?: TypeMapping +): MapReply { + const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined; - return message; -} + const inferred = reply as unknown as UnwrapReply -export interface StreamMessageReply { - id: RedisCommandArgument; - message: Record; -} + switch (mapType) { + case Array: { + return reply as unknown as MapReply; + } + case Map: { + const ret = new Map; -export function transformStreamMessageReply([id, message]: Array): StreamMessageReply { - return { - id, - message: transformTuplesReply(message) - }; -} + for (let i = 0; i < inferred.length; i += 2) { + ret.set(inferred[i].toString(), inferred[i + 1] as any); + } -export function transformStreamMessageNullReply(reply: Array): StreamMessageReply | null { - if (reply === null) return null; - return transformStreamMessageReply(reply); -} + return ret as unknown as MapReply;; + } + default: { + const ret: Record = Object.create(null); + 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 type RedisVariadicArgument = RedisArgument | Array; -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'); - } - } +export function pushVariadicArgument( + args: Array, + value: RedisVariadicArgument +): CommandArguments { + if (Array.isArray(value)) { + args.push(value.length.toString(), ...value); + } else { + args.push('1', value); + } - return args; + return args; } -export type GeoUnits = 'm' | 'km' | 'mi' | 'ft'; +export function parseOptionalVariadicArgument( + parser: CommandParser, + name: RedisArgument, + value?: RedisVariadicArgument +) { + if (value === undefined) return; -export interface GeoCoordinates { - longitude: string | number; - latitude: string | number; -} - -type GeoSearchFromMember = string; - -export type GeoSearchFrom = GeoSearchFromMember | GeoCoordinates; + parser.push(name); -interface GeoSearchByRadius { - radius: number; - unit: GeoUnits; + parser.pushVariadicWithLength(value); } -interface GeoSearchByBox { - width: number; - height: number; - unit: GeoUnits; +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 type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox; - -export interface GeoSearchOptions { - SORT?: 'ASC' | 'DESC'; - COUNT?: GeoCountArgument; +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 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); - } +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 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 enum RedisFunctionFlags { + NO_WRITES = 'no-writes', + ALLOW_OOM = 'allow-oom', + ALLOW_STALE = 'allow-stale', + NO_CLUSTER = 'no-cluster' +} - pushGeoCountArgument(args, options?.COUNT); +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 interface GeoRadiusStoreOptions extends GeoSearchOptions { - STOREDIST?: boolean; +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 function pushGeoRadiusStoreArguments( - args: RedisCommandArguments, - key: RedisCommandArgument, - from: GeoSearchFrom, - radius: number, - unit: GeoUnits, - destination: RedisCommandArgument, - options?: GeoRadiusStoreOptions -): RedisCommandArguments { - pushGeoRadiusArguments(args, key, from, radius, unit, options); +export interface SlotRange { + start: number; + end: number; +} - if (options?.STOREDIST) { - args.push('STOREDIST', destination); - } else { - args.push('STORE', destination); +function parseSlotRangeArguments( + parser: CommandParser, + range: SlotRange +): void { + parser.push( + range.start.toString(), + range.end.toString() + ); +} + +export function parseSlotRangesArguments( + parser: CommandParser, + ranges: SlotRange | Array +) { + if (Array.isArray(ranges)) { + for (const range of ranges) { + parseSlotRangeArguments(parser, range); } - - return args; + } else { + parseSlotRangeArguments(parser, ranges); + } } -export enum GeoReplyWith { - DISTANCE = 'WITHDIST', - HASH = 'WITHHASH', - COORDINATES = 'WITHCOORD' +export type RawRangeReply = [ + start: number, + end: number +]; + +export interface RangeReply { + start: number; + end: number; } -export interface GeoReplyWithMember { - member: string; - distance?: number; - hash?: string; - coordinates?: { - longitude: string; - latitude: string; - }; +export function transformRangeReply([start, end]: RawRangeReply): RangeReply { + return { + start, + end + }; } -export function transformGeoMembersWithReply(reply: Array>, replyWith: Array): Array { - const replyWithSet = new Set(replyWith); +export type ZKeyAndWeight = { + key: RedisArgument; + weight: number; +}; - let index = 0; - const distanceIndex = replyWithSet.has(GeoReplyWith.DISTANCE) && ++index, - hashIndex = replyWithSet.has(GeoReplyWith.HASH) && ++index, - coordinatesIndex = replyWithSet.has(GeoReplyWith.COORDINATES) && ++index; +export type ZVariadicKeys = T | [T, ...Array]; - return reply.map(member => { - const transformedMember: GeoReplyWithMember = { - member: member[0] - }; +export type ZKeys = ZVariadicKeys | ZVariadicKeys; - if (distanceIndex) { - transformedMember.distance = member[distanceIndex]; - } +export function parseZKeysArguments( + parser: CommandParser, + keys: ZKeys +) { + if (Array.isArray(keys)) { + parser.push(keys.length.toString()); - if (hashIndex) { - transformedMember.hash = member[hashIndex]; + if (keys.length) { + if (isPlainKeys(keys)) { + parser.pushKeys(keys); + } else { + for (let i = 0; i < keys.length; i++) { + parser.pushKey(keys[i].key) } - - if (coordinatesIndex) { - const [longitude, latitude] = member[coordinatesIndex]; - transformedMember.coordinates = { - longitude, - latitude - }; + parser.push('WEIGHTS'); + for (let i = 0; i < keys.length; i++) { + parser.push(transformDoubleArgument(keys[i].weight)); } - - return transformedMember; - }); -} - -export function transformEXAT(EXAT: number | Date): string { - return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString(); -} - -export function transformPXAT(PXAT: number | Date): string { - return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString(); -} - -export interface EvalOptions { - keys?: Array; - arguments?: Array; -} - -export function evalFirstKeyIndex(options?: EvalOptions): string | undefined { - return options?.keys?.[0]; -} - -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); + } } + } else { + parser.push('1'); - return args; -} - -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); + if (isPlainKey(keys)) { + parser.pushKey(keys); } else { - args.push(value); + parser.pushKey(keys.key); + parser.push('WEIGHTS', transformDoubleArgument(keys.weight)); } - - return args; + } } -export function pushVerdictNumberArguments( - args: RedisCommandArguments, - value: number | Array -): RedisCommandArguments { - if (Array.isArray(value)) { - for (const item of value) { - args.push(item.toString()); - } - } else { - args.push(value.toString()); - } - - return args; +function isPlainKey(key: RedisArgument | ZKeyAndWeight): key is RedisArgument { + return typeof key === 'string' || key instanceof Buffer; } -export function pushVerdictArgument( - args: RedisCommandArguments, - value: RedisCommandArgument | Array -): RedisCommandArguments { - if (Array.isArray(value)) { - args.push(value.length.toString(), ...value); - } else { - args.push('1', value); - } - - return args; +function isPlainKeys(keys: Array | Array): keys is Array { + return isPlainKey(keys[0]); } -export function pushOptionalVerdictArgument( - args: RedisCommandArguments, - name: RedisCommandArgument, - value: undefined | RedisCommandArgument | Array -): RedisCommandArguments { - if (value === undefined) return args; - - args.push(name); +export type Tail = T extends [infer Head, ...infer Tail] ? Tail : never; - return pushVerdictArgument(args, value); -} - -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. -} +/** + * @deprecated + */ +export function parseArgs(command: Command, ...args: Array): CommandArguments { + const parser = new BasicCommandParser(); + command.parseCommand!(parser, ...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' + const redisArgs: CommandArguments = parser.redisArgs; + if (parser.preserve) { + redisArgs.preserve = parser.preserve; + } + return redisArgs; } -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..5cd81331a4e 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -1,91 +1,1041 @@ -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 HGETDEL from './HGETDEL'; +import HGETEX from './HGETEX'; +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 HSETEX from './HSETEX'; +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, + HGETDEL, + hGetDel: HGETDEL, + HGETEX, + hGetEx: HGETEX, + 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, + HSETEX, + hSetEx: HSETEX, + 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..3d45a02fb4d 100644 --- a/packages/client/lib/multi-command.ts +++ b/packages/client/lib/multi-command.ts @@ -1,95 +1,70 @@ -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'); - } + private readonly typeMapping?: TypeMapping; - readonly queue: Array = []; + constructor(typeMapping?: TypeMapping) { + this.typeMapping = typeMapping; + } - readonly scriptsInUse = new Set(); + readonly queue: Array = []; - addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): void { - this.queue.push({ - args, - transformReply - }); - } + readonly scriptsInUse = new Set(); - 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; - } - - 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 - ); - } + addCommand(args: CommandArguments, transformReply?: TransformReply) { + this.queue.push({ + args, + transformReply + }); + } - if (script.NUMBER_OF_KEYS !== undefined) { - transformedArguments.push(script.NUMBER_OF_KEYS.toString()); - } - - const scriptArguments = script.transformArguments(...args); - transformedArguments.push(...scriptArguments); - if (scriptArguments.preserve) { - transformedArguments.preserve = scriptArguments.preserve; - } - - this.addCommand( - transformedArguments, - script.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); + } - return transformedArguments; + if (script.NUMBER_OF_KEYS !== undefined) { + redisArgs.push(script.NUMBER_OF_KEYS.toString()); } - handleExecReplies(rawReplies: Array): Array { - const execReply = rawReplies[rawReplies.length - 1] as (null | Array); - if (execReply === null) { - throw new WatchError(); - } + redisArgs.push(...args); - return this.transformReplies(execReply); - } + this.addCommand(redisArgs, transformReply); + } + + transformReplies(rawReplies: Array): Array { + const errorIndexes: Array = [], + replies = rawReplies.map((reply, i) => { + if (reply instanceof ErrorReply) { + errorIndexes.push(i); + return reply; + } - 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; - }); + const { transformReply, args } = this.queue[i]; + return transformReply ? transformReply(reply, args.preserve, this.typeMapping) : 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..84997ac7d8f --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts @@ -0,0 +1,13 @@ +import { RedisArgument, MapReply, BlobStringReply, Command } from '../../RESP/types'; +import { CommandParser } from '../../client/parser'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('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..65f438de132 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts @@ -0,0 +1,9 @@ +import { CommandParser } from '../../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; + +export default { + 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 new file mode 100644 index 00000000000..127449264d8 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts @@ -0,0 +1,24 @@ +import { CommandParser } from '../../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, MapReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('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..4550b9498b3 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts @@ -0,0 +1,24 @@ +import { CommandParser } from '../../client/parser'; +import { RedisArgument, ArrayReply, MapReply, BlobStringReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('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..b4e8f843ea6 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; + +export type SentinelSetOptions = Array<{ + option: RedisArgument; + value: RedisArgument; +}>; + +export default { + parseCommand(parser: CommandParser, dbname: RedisArgument, options: SentinelSetOptions) { + parser.push('SENTINEL', 'SET', dbname); + + for (const option of options) { + parser.push(option.option, option.value); + } + }, + 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..cf9228c261f --- /dev/null +++ b/packages/client/lib/sentinel/index.spec.ts @@ -0,0 +1,974 @@ +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 { 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'; +const execAsync = promisify(exec); + +[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.acquire(); + 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.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.acquire(); + 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; + 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(); + } + } +} + +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); + } + 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); + + 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')); + } + ); + }); + + // 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; + }) + + 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.acquire(); + tracer.push("acquired 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(60000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push("connected"); + + const client = await sentinel.acquire(); + 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"); + }); + + + // 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; + }) + + 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(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; + }) + + 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(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; + }) + + 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(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; + }) + + 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(60000); + + 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(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; + }) + + 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(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); + } + } + }); + + 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 new file mode 100644 index 00000000000..3bf94abd819 --- /dev/null +++ b/packages/client/lib/sentinel/index.ts @@ -0,0 +1,1540 @@ +import { EventEmitter } from 'node:events'; +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'; +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; + + /** + * 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; + } + + #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 = {} + >( + options: RedisSentinelOptions, + internal: RedisSentinelInternal, + clientInfo: ClientInfo, + commandOptions?: CommandOptions, + ) { + 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) + ); + } + + /** + * @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; + + /** + * 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'); + } + + 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; + + /** + * 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; + } + + 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); + // Create new commandOptions object with the inherited properties + proxy._self.#commandOptions = { + ...(this._self.#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.#options, this._self.#internal, clientInfo, this._self.#commandOptions) + ); + } 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) + ); + } + + /** + * @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; + + /** + * 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); + } + + 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 + } + }); + } + + /** + * 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) { + return { id }; + } + + 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 + 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"); + + count+=1; + 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 || client.isDirtyWatch); + + 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 + } + }); + } +} \ No newline at end of file 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..e70dc45c790 --- /dev/null +++ b/packages/client/lib/sentinel/multi-commands.ts @@ -0,0 +1,250 @@ +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'; +import { BasicCommandParser } from '../client/parser'; +import { Tail } from '../commands/generic-transformers'; + +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: Tail>) => 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 parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + + 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 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, + transformReply + ); + }; + } + + private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + + return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { + 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, + transformReply + ); + }; + } + + private static _createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); + + return function (this: RedisSentinelMultiCommand, ...args: Array) { + 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 + ); + }; + } + + 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 + }); + } + + readonly #multi = new RedisMultiCommand(); + readonly #sentinel: RedisSentinelType + #isReadonly: boolean | undefined = true; + + constructor(sentinel: RedisSentinelType, typeMapping: TypeMapping) { + this.#multi = new RedisMultiCommand(typeMapping); + this.#sentinel = sentinel; + } + + #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; + } + + #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 + ) + ) 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 + ) + ) 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..60696bc3437 --- /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); + } + } + } +} \ No newline at end of file diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts new file mode 100644 index 00000000000..28a5a91ddd3 --- /dev/null +++ b/packages/client/lib/sentinel/types.ts @@ -0,0 +1,183 @@ +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; + // 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; + // 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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..90b789ddca9 --- /dev/null +++ b/packages/client/lib/sentinel/utils.ts @@ -0,0 +1,98 @@ +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'; +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 parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + return this._self._execute( + command.IS_READ_ONLY, + client => client._executeCommand(command, parser, this.commandOptions, transformReply) + ); + }; +} + +export function createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + + return async function (this: T, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + return this._self._execute( + fn.IS_READ_ONLY, + client => client._executeCommand(fn, parser, this._self.commandOptions, transformReply) + ); + } +}; + +export function createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + + return async function (this: T, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + return this._self._execute( + command.IS_READ_ONLY, + client => client._executeCommand(command, parser, this._self.commandOptions, transformReply) + ); + } +}; + +export function createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + + return async function (this: T, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args); + + return this._self._execute( + script.IS_READ_ONLY, + client => client._executeScript(script, parser, this.commandOptions, transformReply) + ); + }; +} 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..63f43ba5e6a 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -1,11 +1,15 @@ import TestUtils from '@redis/test-utils'; import { SinonSpy } from 'sinon'; -import { promiseTimeout } from './utils'; - -const utils = new TestUtils({ - dockerImageName: 'redis', +import { setTimeout } from 'node:timers/promises'; +import { CredentialsProvider } from './authx'; +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', - defaultDockerVersion: '7.4-rc2' + defaultDockerVersion: '8.0-M05-pre' }); export default utils; @@ -14,50 +18,175 @@ const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; -export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: [...DEBUG_MODE_ARGS] - }, - PASSWORD: { - serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], - clientOptions: { - password: 'password' - } +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; + +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: { + serverArguments: [...DEBUG_MODE_ARGS] + }, + PASSWORD: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + password: 'password' + } + }, + ASYNC_BASIC_AUTH: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + credentialsProvider: asyncBasicAuthCredentialsProvider + } }, - 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 - } + STREAMING_AUTH: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + credentialsProvider: streamingCredentialsProvider + } + } + }, + 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 + } + } + }, + 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, } + } }; 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 setTimeout(50); + } while (spy.callCount === calls); +} + +export const BLOCKING_MIN_VALUE = ( + utils.isVersionGreaterThan([7]) ? Number.MIN_VALUE : + utils.isVersionGreaterThan([6]) ? 0.01 : + 1 +); - await promiseTimeout(50); - } while (spy.callCount === calls); +export function parseFirstKey(command: Command, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand!(parser, ...args); + return parser.firstKey; } 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..34d60154083 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,42 +1,26 @@ { "name": "@redis/client", - "version": "1.6.0", + "version": "5.0.1", "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/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/graph/.release-it.json b/packages/entraid/.release-it.json similarity index 65% rename from packages/graph/.release-it.json rename to packages/entraid/.release-it.json index 530d8f355d4..a5f3a31062e 100644 --- a/packages/graph/.release-it.json +++ b/packages/entraid/.release-it.json @@ -1,10 +1,11 @@ { "git": { - "tagName": "graph@${version}", + "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..733cf895a7e --- /dev/null +++ b/packages/entraid/README.md @@ -0,0 +1,188 @@ +# @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 + - DefaultAzureCredential from @azure/identity +- Built-in retry mechanisms for transient failures + +## Installation + + +```bash +npm install "@redis/client@5.0.0-next.7" +npm install "@redis/entraid@5.0.0-next.7" +``` + +## 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 +- `createForDefaultAzureCredential`: Use when you want to leverage Azure Identity's DefaultAzureCredential + +## 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 + } +}); +``` + +### 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 { getDefaultAzureCredential } from '@azure/identity'; +import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid'; + +// Create a DefaultAzureCredential instance +const credential = getDefaultAzureCredential(); + +// 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 + +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/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 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..4d078a01ede --- /dev/null +++ b/packages/entraid/integration-tests/entraid-integration.spec.ts @@ -0,0 +1,262 @@ +import { DefaultAzureCredential, EnvironmentCredential } from '@azure/identity'; +import { BasicAuth } from '@redis/client/dist/lib/authx'; +import { createClient } from '@redis/client'; +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'; +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 + } + }) + ); + }); + + 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; + 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 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 + }; + }; + + 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); + } + }; + + /** + * 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() + ); + + return { + token: creds.password, + exp: tokenPayload.exp, + iat: tokenPayload.iat, + lifetime: tokenPayload.exp - tokenPayload.iat, + uti: tokenPayload.uti + }; + }); + + // 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: ${(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: ${(Array.from(uniqueUti).join(','))}` + ); + } + }; + + 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, options.testingDefaultAzureCredential); + } 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/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 new file mode 100644 index 00000000000..98a3a11078a --- /dev/null +++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts @@ -0,0 +1,417 @@ +import type { GetTokenOptions, TokenCredential } from '@azure/core-auth'; +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, 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'; + +/** + * 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: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError + } + ); + } + + /** + * 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: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError + }); + } + + /** + * 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 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 + */ + 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, + credentialsMapper: params.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError + }); + } + }; + } + + 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'); + } + } + +} + +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 } + | { 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 + 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 & { + 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 + ); + } +} + 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..465c9e8a975 --- /dev/null +++ b/packages/entraid/lib/entraid-credentials-provider.ts @@ -0,0 +1,195 @@ +import { AuthenticationResult } from '@azure/msal-common/node'; +import { AccessToken } from '@azure/core-auth'; +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 type AuthenticationResponse = AuthenticationResult | AccessToken + +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: AuthenticationResponse) => 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: AuthenticationResponse) => 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 | AccessToken }): 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 { 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) + ); + 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; + } + +} + +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); + +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/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..0f15e01fcdc --- /dev/null +++ b/packages/entraid/lib/msal-identity-provider.ts @@ -0,0 +1,25 @@ +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> { + 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() + }; + } + +} diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts new file mode 100644 index 00000000000..11ad498f0b3 --- /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 = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' +}); + +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..57af37d36cd --- /dev/null +++ b/packages/entraid/package.json @@ -0,0 +1,49 @@ +{ + "name": "@redis/entraid", + "version": "5.0.1", + "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", + "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": { + "@redis/client": "^5.0.1" + }, + "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/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`); +}); 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/graph/tsconfig.json b/packages/entraid/tsconfig.json similarity index 64% rename from packages/graph/tsconfig.json rename to packages/entraid/tsconfig.json index 9d17cb63371..47100f5b87d 100644 --- a/packages/graph/tsconfig.json +++ b/packages/entraid/tsconfig.json @@ -4,17 +4,18 @@ "outDir": "./dist" }, "include": [ - "./lib/**/*.ts" + "./lib/**/*.ts", + "./index.ts" ], "exclude": [ - "./lib/test-utils.ts", - "./lib/**/*.spec.ts" + "./lib/**/*.spec.ts", + "./lib/test-util.ts", ], "typedocOptions": { "entryPoints": [ "./lib" ], "entryPointStrategy": "expand", - "out": "../../documentation/graph" + "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/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/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 6e1fa74e219..00000000000 --- a/packages/graph/lib/commands/CONFIG_GET.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_GET'; - -describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts deleted file mode 100644 index ce80a1148ed..00000000000 --- a/packages/graph/lib/commands/CONFIG_GET.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const IS_READ_ONLY = true; - -export function transformArguments(configKey: string): Array { - return ['GRAPH.CONFIG', 'GET', configKey]; -} - -type ConfigItem = [ - configKey: string, - value: number -]; - -export declare function transformReply(): ConfigItem | Array; 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 51dce0a8cd9..00000000000 --- a/packages/graph/lib/commands/CONFIG_SET.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_SET'; - -describe('CONFIG SET', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts deleted file mode 100644 index ac81449ad15..00000000000 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function transformArguments(configKey: string, value: number): Array { - return [ - 'GRAPH.CONFIG', - 'SET', - configKey, - value.toString() - ]; -} - -export declare function transformReply(): 'OK'; diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts deleted file mode 100644 index e51ac2bfab8..00000000000 --- a/packages/graph/lib/commands/DELETE.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DELETE'; - -describe('', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GRAPH.DELETE', 'key'] - ); - }); - - testUtils.testWithClient('client.graph.delete', async client => { - await client.graph.query('key', 'RETURN 1'); - - assert.equal( - typeof await client.graph.delete('key'), - '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 240708143c6..00000000000 --- a/packages/graph/lib/commands/DELETE.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string): Array { - return ['GRAPH.DELETE', key]; -} - -export declare function transformReply(): string; diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts deleted file mode 100644 index 86d89b212cb..00000000000 --- a/packages/graph/lib/commands/EXPLAIN.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXPLAIN'; - -describe('EXPLAIN', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts deleted file mode 100644 index ebea9ca900d..00000000000 --- a/packages/graph/lib/commands/EXPLAIN.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, query: string): Array { - return ['GRAPH.EXPLAIN', key, query]; -} - -export declare function transformReply(): Array; diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts deleted file mode 100644 index d4fab0358b9..00000000000 --- a/packages/graph/lib/commands/LIST.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LIST'; - -describe('LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['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 1939d43d889..00000000000 --- a/packages/graph/lib/commands/LIST.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const IS_READ_ONLY = true; - -export function transformArguments(): Array { - return ['GRAPH.LIST']; -} - -export declare function transformReply(): Array; diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts deleted file mode 100644 index 80857eb0ab9..00000000000 --- a/packages/graph/lib/commands/PROFILE.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PROFILE'; - -describe('PROFILE', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts deleted file mode 100644 index c964452f497..00000000000 --- a/packages/graph/lib/commands/PROFILE.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, query: string): Array { - return ['GRAPH.PROFILE', key, query]; -} - -export declare function transformReply(): Array; diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts deleted file mode 100644 index c8a9a20372b..00000000000 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './QUERY'; - -describe('QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'query'), - ['GRAPH.QUERY', 'key', 'query'] - ); - }); - - 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 741cc6a3601..00000000000 --- a/packages/graph/lib/commands/QUERY.ts +++ /dev/null @@ -1,55 +0,0 @@ -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 - ); -} - -type Headers = Array; - -type Data = Array; - -type Metadata = Array; - -type QueryRawReply = [ - headers: Headers, - data: Data, - metadata: Metadata -] | [ - metadata: Metadata -]; - -export type QueryReply = { - headers: undefined; - data: undefined; - metadata: Metadata; -} | { - headers: Headers; - data: Data; - metadata: Metadata; -}; - -export function transformReply(reply: QueryRawReply): QueryReply { - return reply.length === 1 ? { - headers: undefined, - data: undefined, - metadata: reply[0] - } : { - headers: reply[0], - data: reply[1], - metadata: reply[2] - }; -} 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 1d76b1bd652..00000000000 --- a/packages/graph/lib/commands/RO_QUERY.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RO_QUERY'; - -describe('RO_QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); \ 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 d4dda9dee27..00000000000 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ /dev/null @@ -1,23 +0,0 @@ -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'; diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts deleted file mode 100644 index e3083b994d6..00000000000 --- a/packages/graph/lib/commands/SLOWLOG.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SLOWLOG'; - -describe('SLOWLOG', () => { - it('transformArguments', () => { - assert.deepEqual( - 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); -}); diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts deleted file mode 100644 index 6ae87af89bf..00000000000 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const IS_READ_ONLY = true; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string) { - 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), - command, - query, - took: Number(took) - })); -} 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 deleted file mode 100644 index 2acf9089ee6..00000000000 --- a/packages/graph/lib/commands/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -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'; - -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}`) - } -} diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts deleted file mode 100644 index 495c6d17a8a..00000000000 --- a/packages/graph/lib/graph.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { strict as assert } from '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 a95338bd8f3..00000000000 --- a/packages/graph/lib/graph.ts +++ /dev/null @@ -1,359 +0,0 @@ -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'; - -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 = Omit & { - data?: Array; -}; - -export type GraphClientType = RedisClientType<{ - graph: { - query: typeof import('./commands/QUERY'), - roQuery: typeof import('./commands/RO_QUERY') - } -}, 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}]`); - return value; - } - - async #parseReply(reply: QueryReply): 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 56c0af56a2e..00000000000 --- a/packages/graph/lib/test-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import TestUtils from '@redis/test-utils'; -import RedisGraph from '.'; - -export default new TestUtils({ - dockerImageName: 'redislabs/redisgraph', - dockerImageVersionArgument: 'redisgraph-version' -}); - -export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisgraph.so'], - clientOptions: { - modules: { - graph: RedisGraph - } - } - } - } -}; diff --git a/packages/graph/package.json b/packages/graph/package.json deleted file mode 100644 index 95cce6b8a86..00000000000 --- a/packages/graph/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@redis/graph", - "version": "1.1.1", - "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist/" - ], - "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" - }, - "peerDependencies": { - "@redis/client": "^1.0.0" - }, - "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" - }, - "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/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..b2c22e0b9c0 100644 --- a/packages/json/lib/commands/ARRAPPEND.spec.ts +++ b/packages/json/lib/commands/ARRAPPEND.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(ARRAPPEND, '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( + parseArgs(ARRAPPEND, '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..539eb91a297 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,15 +1,23 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; 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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + ...jsons: Array + ) { + parser.push('JSON.ARRAPPEND'); + parser.pushKey(key); + parser.push(path, 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)); + for (let i = 0; i < jsons.length; i++) { + parser.push(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..3c1377354f1 100644 --- a/packages/json/lib/commands/ARRINDEX.spec.ts +++ b/packages/json/lib/commands/ARRINDEX.spec.ts @@ -1,37 +1,49 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(ARRINDEX, '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( + parseArgs(ARRINDEX, 'key', '$', 'value', { + range: { + start: 0 + } + }), + ['JSON.ARRINDEX', 'key', '$', '"value"', '0'] + ); + }); + it('with start and stop', () => { assert.deepEqual( - await client.json.arrIndex('key', '$', 'json'), - [-1] + parseArgs(ARRINDEX, '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..23f010b1f0b 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,21 +1,34 @@ +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 '.'; -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 { - const args = ['JSON.ARRINDEX', key, path, transformRedisJsonArgument(json)]; +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + options?: JsonArrIndexOptions + ) { + parser.push('JSON.ARRINDEX'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(json)); - if (start !== undefined && start !== null) { - args.push(start.toString()); + if (options?.range) { + parser.push(options.range.start.toString()); - if (stop !== undefined && stop !== null) { - args.push(stop.toString()); - } + if (options.range.stop !== undefined) { + parser.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..bf9c8a2a051 100644 --- a/packages/json/lib/commands/ARRINSERT.spec.ts +++ b/packages/json/lib/commands/ARRINSERT.spec.ts @@ -1,30 +1,31 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(ARRINSERT, '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( + parseArgs(ARRINSERT, '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..eb1d7c882f2 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,15 +1,24 @@ +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 '.'; -export const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + path: RedisArgument, + index: number, + json: RedisJSON, + ...jsons: Array + ) { + parser.push('JSON.ARRINSERT'); + parser.pushKey(key); + parser.push(path, index.toString(), 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)); + for (let i = 0; i < jsons.length; i++) { + parser.push(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..dcf7d35acb0 100644 --- a/packages/json/lib/commands/ARRLEN.spec.ts +++ b/packages/json/lib/commands/ARRLEN.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 './ARRLEN'; +import ARRLEN from './ARRLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ARRLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.ARRLEN', 'key'] - ); - }); +describe('JSON.ARRLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ARRLEN, 'key'), + ['JSON.ARRLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.ARRLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(ARRLEN, '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..f49166c218d 100644 --- a/packages/json/lib/commands/ARRLEN.ts +++ b/packages/json/lib/commands/ARRLEN.ts @@ -1,15 +1,18 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.ARRLEN', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonArrLenOptions { + path?: RedisArgument; } -export declare function transformReply(): number | Array; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonArrLenOptions) { + parser.push('JSON.ARRLEN'); + parser.pushKey(key); + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..f823e7fc08a 100644 --- a/packages/json/lib/commands/ARRPOP.spec.ts +++ b/packages/json/lib/commands/ARRPOP.spec.ts @@ -1,57 +1,67 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('JSON.ARRPOP', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(ARRPOP, 'key'), + ['JSON.ARRPOP', 'key'] + ); + }); + + it('with path', () => { + assert.deepEqual( + parseArgs(ARRPOP, '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( + parseArgs(ARRPOP, '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..5f1489c3bd4 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,27 +1,31 @@ -import { RedisJSON, transformRedisJsonNullReply } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string, path?: string, index?: number): Array { - const args = ['JSON.ARRPOP', key]; - - if (path) { - args.push(path); - - if (index !== undefined && index !== null) { - args.push(index.toString()); - } - } - - return args; +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 '.'; + +export interface RedisArrPopOptions { + path: RedisArgument; + index?: number; } -export function transformReply(reply: null | string | Array): null | RedisJSON | Array { - if (reply === null) return null; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: RedisArrPopOptions) { + parser.push('JSON.ARRPOP'); + parser.pushKey(key); - if (Array.isArray(reply)) { - return reply.map(transformRedisJsonNullReply); + if (options) { + parser.push(options.path); + + if (options.index !== undefined) { + parser.push(options.index.toString()); + } } + }, + transformReply(reply: NullReply | BlobStringReply | ArrayReply) { + return isArrayReply(reply) ? + (reply as unknown as UnwrapReply).map(item => transformRedisJsonNullReply(item)) : + transformRedisJsonNullReply(reply); + } +} as const satisfies Command; - return transformRedisJsonNullReply(reply); -} diff --git a/packages/json/lib/commands/ARRTRIM.spec.ts b/packages/json/lib/commands/ARRTRIM.spec.ts index c254e1b6a03..e346716e8df 100644 --- a/packages/json/lib/commands/ARRTRIM.spec.ts +++ b/packages/json/lib/commands/ARRTRIM.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ARRTRIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 0, 1), - ['JSON.ARRTRIM', 'key', '$', '0', '1'] - ); - }); +describe('JSON.ARRTRIM', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARRTRIM, '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..573fa787507 100644 --- a/packages/json/lib/commands/ARRTRIM.ts +++ b/packages/json/lib/commands/ARRTRIM.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - return ['JSON.ARRTRIM', key, path, start.toString(), stop.toString()]; -} - -export declare function transformReply(): number | Array; +export default { + IS_READ_ONLY: false, + 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 new file mode 100644 index 00000000000..c1786cc1dde --- /dev/null +++ b/packages/json/lib/commands/CLEAR.spec.ts @@ -0,0 +1,33 @@ +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( + parseArgs(CLEAR, 'key'), + ['JSON.CLEAR', 'key'] + ); + }); + + it('with path', () => { + assert.deepEqual( + parseArgs(CLEAR, '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..b86513cc219 --- /dev/null +++ b/packages/json/lib/commands/CLEAR.ts @@ -0,0 +1,19 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; + +export interface JsonClearOptions { + path?: RedisArgument; +} + +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonClearOptions) { + parser.push('JSON.CLEAR'); + parser.pushKey(key); + + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..09c29328d8e 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.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 './DEBUG_MEMORY'; +import DEBUG_MEMORY from './DEBUG_MEMORY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(DEBUG_MEMORY, 'key'), + ['JSON.DEBUG', 'MEMORY', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.DEBUG', 'MEMORY', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(DEBUG_MEMORY, '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..aa36d74c077 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.ts @@ -1,13 +1,19 @@ -export const FIRST_KEY_INDEX = 2; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.DEBUG', 'MEMORY', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonDebugMemoryOptions { + path?: RedisArgument; } -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDebugMemoryOptions) { + parser.push('JSON.DEBUG', 'MEMORY'); + parser.pushKey(key); + + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..a008c3b9b2b 100644 --- a/packages/json/lib/commands/DEL.spec.ts +++ b/packages/json/lib/commands/DEL.spec.ts @@ -1,28 +1,32 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DEL', () => { - describe('transformArguments', () => { - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.DEL', 'key'] - ); - }); +describe('JSON.DEL', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(DEL, 'key'), + ['JSON.DEL', 'key'] + ); + }); - it('key, path', () => { - assert.deepEqual( - transformArguments('key', '$.path'), - ['JSON.DEL', 'key', '$.path'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(DEL, '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..e86366bebe6 100644 --- a/packages/json/lib/commands/DEL.ts +++ b/packages/json/lib/commands/DEL.ts @@ -1,13 +1,19 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.DEL', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonDelOptions { + path?: RedisArgument } -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDelOptions) { + parser.push('JSON.DEL'); + parser.pushKey(key); + + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..888fff5659b 100644 --- a/packages/json/lib/commands/FORGET.spec.ts +++ b/packages/json/lib/commands/FORGET.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 './FORGET'; +import FORGET from './FORGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('FORGET', () => { - describe('transformArguments', () => { - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.FORGET', 'key'] - ); - }); +describe('JSON.FORGET', () => { + describe('transformArguments', () => { + it('key', () => { + assert.deepEqual( + parseArgs(FORGET, 'key'), + ['JSON.FORGET', 'key'] + ); + }); - it('key, path', () => { - assert.deepEqual( - transformArguments('key', '$.path'), - ['JSON.FORGET', 'key', '$.path'] - ); - }); + it('key, path', () => { + assert.deepEqual( + parseArgs(FORGET, '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..0a8ed3d91c4 100644 --- a/packages/json/lib/commands/FORGET.ts +++ b/packages/json/lib/commands/FORGET.ts @@ -1,13 +1,19 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.FORGET', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonForgetOptions { + path?: RedisArgument; } -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonForgetOptions) { + parser.push('JSON.FORGET'); + parser.pushKey(key); + + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..0741de316e1 100644 --- a/packages/json/lib/commands/GET.spec.ts +++ b/packages/json/lib/commands/GET.spec.ts @@ -1,78 +1,38 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('JSON.GET', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(GET, '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( + parseArgs(GET, 'key', { path: '$' }), + ['JSON.GET', 'key', '$'] ); - }, GLOBAL.SERVERS.OPEN); + }); + + it('array', () => { + assert.deepEqual( + parseArgs(GET, '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..d43d7464c50 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,42 +1,20 @@ -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 '.'; -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); +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonGetOptions) { + parser.push('JSON.GET'); + parser.pushKey(key); + if (options?.path !== undefined) { + parser.pushVariadic(options.path) } - - if (options?.SPACE) { - args.push('SPACE', options.SPACE); - } - - if (options?.NOESCAPE) { - args.push('NOESCAPE'); - } - - 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..30a092035c5 100644 --- a/packages/json/lib/commands/MERGE.spec.ts +++ b/packages/json/lib/commands/MERGE.spec.ts @@ -1,21 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('MERGE', () => { - testUtils.isVersionGreaterThanHook([2, 6]); +describe('JSON.MERGE', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MERGE, '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..0cb8131a68c 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,9 +1,13 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: false, + 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 456e160dd50..2d8efafde71 100644 --- a/packages/json/lib/commands/MGET.spec.ts +++ b/packages/json/lib/commands/MGET.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('MGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['1', '2'], '$'), - ['JSON.MGET', '1', '2', '$'] - ); - }); +describe('JSON.MGET', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MGET, ['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..447de064d2b 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,17 +1,15 @@ -import { RedisJSON, transformRedisJsonNullReply } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(keys: Array, path: string): Array { - return [ - 'JSON.MGET', - ...keys, - path - ]; -} - -export function transformReply(reply: Array): Array { - return reply.map(transformRedisJsonNullReply); -} +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 '.'; + +export default { + IS_READ_ONLY: true, + 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)) + } +} as const satisfies Command; diff --git a/packages/json/lib/commands/MSET.spec.ts b/packages/json/lib/commands/MSET.spec.ts index 53d4d822505..38e8b077e81 100644 --- a/packages/json/lib/commands/MSET.spec.ts +++ b/packages/json/lib/commands/MSET.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MSET, [{ + 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..cb0bea26ddd 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,28 +1,22 @@ +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 { 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); - args[0] = 'JSON.MSET'; +export default { + IS_READ_ONLY: false, + 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; -} - -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..b438069e80f 100644 --- a/packages/json/lib/commands/NUMINCRBY.spec.ts +++ b/packages/json/lib/commands/NUMINCRBY.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('NUMINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 1), - ['JSON.NUMINCRBY', 'key', '$', '1'] - ); - }); +describe('JSON.NUMINCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(NUMINCRBY, '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..02c1c17dbc9 100644 --- a/packages/json/lib/commands/NUMINCRBY.ts +++ b/packages/json/lib/commands/NUMINCRBY.ts @@ -1,7 +1,17 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - return ['JSON.NUMINCRBY', key, path, by.toString()]; -} - -export { transformNumbersReply as transformReply } from '.'; +export default { + IS_READ_ONLY: false, + 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) => { + 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..24ee932e952 100644 --- a/packages/json/lib/commands/NUMMULTBY.spec.ts +++ b/packages/json/lib/commands/NUMMULTBY.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('NUMMULTBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 2), - ['JSON.NUMMULTBY', 'key', '$', '2'] - ); - }); +describe('JSON.NUMMULTBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(NUMMULTBY, '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..c3621908a4c 100644 --- a/packages/json/lib/commands/NUMMULTBY.ts +++ b/packages/json/lib/commands/NUMMULTBY.ts @@ -1,7 +1,13 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import NUMINCRBY from './NUMINCRBY'; -export function transformArguments(key: string, path: string, by: number): Array { - return ['JSON.NUMMULTBY', key, path, by.toString()]; -} - -export { transformNumbersReply as transformReply } from '.'; +export default { + IS_READ_ONLY: false, + 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 6288c112392..0d2176248e4 100644 --- a/packages/json/lib/commands/OBJKEYS.spec.ts +++ b/packages/json/lib/commands/OBJKEYS.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 './OBJKEYS'; +import OBJKEYS from './OBJKEYS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('OBJKEYS', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.OBJKEYS', 'key'] - ); - }); +describe('JSON.OBJKEYS', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(OBJKEYS, 'key'), + ['JSON.OBJKEYS', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.OBJKEYS', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(OBJKEYS, '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..f7e94dd4dfc 100644 --- a/packages/json/lib/commands/OBJKEYS.ts +++ b/packages/json/lib/commands/OBJKEYS.ts @@ -1,13 +1,18 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.OBJKEYS', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonObjKeysOptions { + path?: RedisArgument; } -export declare function transformReply(): Array | null | Array | null>; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjKeysOptions) { + parser.push('JSON.OBJKEYS'); + parser.pushKey(key); + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..a5664a4d6bc 100644 --- a/packages/json/lib/commands/OBJLEN.spec.ts +++ b/packages/json/lib/commands/OBJLEN.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 './OBJLEN'; +import OBJLEN from './OBJLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('OBJLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.OBJLEN', 'key'] - ); - }); +describe('JSON.OBJLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(OBJLEN, 'key'), + ['JSON.OBJLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.OBJLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(OBJLEN, '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..d1286a89b8c 100644 --- a/packages/json/lib/commands/OBJLEN.ts +++ b/packages/json/lib/commands/OBJLEN.ts @@ -1,13 +1,18 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.OBJLEN', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonObjLenOptions { + path?: RedisArgument; } -export declare function transformReply(): number | null | Array; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjLenOptions) { + parser.push('JSON.OBJLEN'); + parser.pushKey(key); + if (options?.path !== undefined) { + parser.push(options.path); + } + }, + 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..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 'assert'; +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..62084d73b0f 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/dist/lib/client/parser"; +import { Command, RedisArgument } from "@redis/client/dist/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 8f8586a2047..7bd927f08e4 100644 --- a/packages/json/lib/commands/SET.spec.ts +++ b/packages/json/lib/commands/SET.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SET', () => { - describe('transformArguments', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 'json'), - ['JSON.SET', 'key', '$', '"json"'] - ); - }); +describe('JSON.SET', () => { + describe('transformArguments', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SET, 'key', '$', 'json'), + ['JSON.SET', 'key', '$', '"json"'] + ); + }); - it('NX', () => { - assert.deepEqual( - transformArguments('key', '$', 'json', { NX: true }), - ['JSON.SET', 'key', '$', '"json"', 'NX'] - ); - }); + it('NX', () => { + assert.deepEqual( + parseArgs(SET, '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( + parseArgs(SET, '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..75d7099acfb 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,25 +1,39 @@ +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 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 { - const args = ['JSON.SET', key, path, transformRedisJsonArgument(json)]; +export default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + options?: JsonSetOptions + ) { + parser.push('JSON.SET'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(json)); - if ((options)?.NX) { - args.push('NX'); - } else if ((options)?.XX) { - args.push('XX'); + if (options?.condition) { + parser.push(options?.condition); + } else if (options?.NX) { + parser.push('NX'); + } else if (options?.XX) { + parser.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..ebd539130e1 100644 --- a/packages/json/lib/commands/STRAPPEND.spec.ts +++ b/packages/json/lib/commands/STRAPPEND.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 './STRAPPEND'; +import STRAPPEND from './STRAPPEND'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(STRAPPEND, 'key', 'append'), + ['JSON.STRAPPEND', 'key', '"append"'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$', 'append'), - ['JSON.STRAPPEND', 'key', '$', '"append"'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(STRAPPEND, '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..45d503856ac 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,21 +1,22 @@ +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 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 { - const args = ['JSON.STRAPPEND', key]; +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, append: string, options?: JsonStrAppendOptions) { + parser.push('JSON.STRAPPEND'); + parser.pushKey(key); - if (append !== undefined && append !== null) { - args.push(pathOrAppend, transformRedisJsonArgument(append)); - } else { - args.push(transformRedisJsonArgument(pathOrAppend)); + if (options?.path !== undefined) { + parser.push(options.path); } - return args; -} - -export declare function transformReply(): number | Array; + 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 cf163d3c19e..b6881b5bd52 100644 --- a/packages/json/lib/commands/STRLEN.spec.ts +++ b/packages/json/lib/commands/STRLEN.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 './STRLEN'; +import STRLEN from './STRLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('STRLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.STRLEN', 'key'] - ); - }); +describe('JSON.STRLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(STRLEN, 'key'), + ['JSON.STRLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.STRLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(STRLEN, '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..644cdf27ef7 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -1,15 +1,19 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['JSON.STRLEN', key]; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonStrLenOptions) { + parser.push('JSON.STRLEN'); + parser.pushKey(key); - if (path) { - args.push(path); + if (options?.path) { + parser.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..173c7708f4a --- /dev/null +++ b/packages/json/lib/commands/TOGGLE.spec.ts @@ -0,0 +1,22 @@ +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( + parseArgs(TOGGLE, '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..85c769729c7 --- /dev/null +++ b/packages/json/lib/commands/TOGGLE.ts @@ -0,0 +1,12 @@ +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, + 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 5cecfb827a7..1b6ad109816 100644 --- a/packages/json/lib/commands/TYPE.spec.ts +++ b/packages/json/lib/commands/TYPE.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 './TYPE'; +import TYPE from './TYPE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TYPE', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.TYPE', 'key'] - ); - }); +describe('JSON.TYPE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(TYPE, 'key'), + ['JSON.TYPE', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.TYPE', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + parseArgs(TYPE, '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..1146043b2c2 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -1,13 +1,25 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.TYPE', key]; - - if (path) { - args.push(path); - } - - return args; +export interface JsonTypeOptions { + path?: RedisArgument; } -export declare function transformReply(): string | null | Array; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonTypeOptions) { + parser.push('JSON.TYPE'); + parser.pushKey(key); + + if (options?.path) { + parser.push(options.path); + } + }, + 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; 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..9894b2d0399 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -1,21 +1,21 @@ import TestUtils from '@redis/test-utils'; import RedisJSON from '.'; -export default new TestUtils({ - dockerImageName: 'redislabs/rejson', - dockerImageVersionArgument: 'rejson-version', - defaultDockerVersion: '2.6.9' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' }); 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..5c2dfc49a45 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,30 +1,24 @@ { "name": "@redis/json", - "version": "1.0.7", + "version": "5.0.1", "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": "^5.0.1" }, "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..f0b2a34905d --- /dev/null +++ b/packages/redis/README.md @@ -0,0 +1,313 @@ +# 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 + +```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.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. + +## 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). + +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..61da052ea20 --- /dev/null +++ b/packages/redis/index.ts @@ -0,0 +1,113 @@ +import { + RedisModules, + RedisFunctions, + RedisScripts, + RespVersions, + TypeMapping, + createClient as genericCreateClient, + RedisClientOptions, + RedisClientType as GenericRedisClientType, + createCluster as genericCreateCluster, + RedisClusterOptions, + RedisClusterType as genericRedisClusterType, + RedisSentinelOptions, + RedisSentinelType as genericRedisSentinelType, + createSentinel as genericCreateSentinel +} from '@redis/client'; +import RedisBloomModules from '@redis/bloom'; +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'; + +const modules = { + ...RedisBloomModules, + 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 = {} +> = GenericRedisClientType; + +export function createClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +>( + options?: RedisClientOptions +): GenericRedisClientType { + return genericCreateClient({ + ...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 = {} +> = genericRedisClusterType; + +export function createCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +>( + options: RedisClusterOptions +): RedisClusterType { + 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, + ...(options?.modules as M) + } + }); +} diff --git a/packages/redis/package.json b/packages/redis/package.json new file mode 100644 index 00000000000..e7c9da2660b --- /dev/null +++ b/packages/redis/package.json @@ -0,0 +1,33 @@ +{ + "name": "redis", + "description": "A modern, high performance Redis client", + "version": "5.0.1", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "!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" + }, + "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..420911c5600 100644 --- a/packages/search/lib/commands/AGGREGATE.spec.ts +++ b/packages/search/lib/commands/AGGREGATE.spec.ts @@ -1,516 +1,522 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; + +describe('AGGREGATE', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*'), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('with VERBATIM', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + VERBATIM: true + }), + ['FT.AGGREGATE', 'index', '*', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT] + ); + }); -describe('AGGREGATE', () => { - describe('transformArguments', () => { - it('without options', () => { + it('with ADDSCORES', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { ADDSCORES: true }), + ['FT.AGGREGATE', 'index', '*', 'ADDSCORES', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + describe('with LOAD', () => { + describe('single', () => { + describe('without alias', () => { + it('string', () => { assert.deepEqual( - transformArguments('index', '*'), - ['FT.AGGREGATE', 'index', '*'] + parseArgs(AGGREGATE, 'index', '*', { + LOAD: '@property' + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); - }); + }); - it('with VERBATIM', () => { + it('{ identifier: string }', () => { assert.deepEqual( - transformArguments('index', '*', { VERBATIM: true }), - ['FT.AGGREGATE', 'index', '*', 'VERBATIM'] + parseArgs(AGGREGATE, 'index', '*', { + LOAD: { + identifier: '@property' + } + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); + }); }); - it('with ADDSCORES', () => { - assert.deepEqual( - transformArguments('index', '*', { ADDSCORES: true }), - ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] - ); + it('with alias', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + LOAD: { + identifier: '@property', + AS: 'alias' + } + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias', 'DIALECT', DEFAULT_DIALECT] + ); }); + }); - 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'] - ); - }); + it('multiple', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + LOAD: ['@1', '@2'] + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + describe('with STEPS', () => { + describe('GROUPBY', () => { + describe('COUNT', () => { + describe('without properties', () => { + it('without alias', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('with alias', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT', + AS: 'count' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + describe('with properties', () => { + it('single', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + properties: '@property', + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] + ); }); it('multiple', () => { - assert.deepEqual( - transformArguments('index', '*', { LOAD: ['@1', '@2'] }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2'] - ); + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + properties: ['@1', '@2'], + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] + ); }); + }); }); - 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( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT_DISTINCT', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT_DISTINCTISH', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'SUM', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('MIN', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'MIN', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('MAX', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'MAX', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('AVG', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'AVG', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + it('STDDEV', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'STDDEV', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('QUANTILE', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'QUANTILE', + property: '@property', + quantile: 0.5 + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5', 'DIALECT', DEFAULT_DIALECT] + ); + }); - describe('LIMIT', () => { + it('TOLIST', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'TOLIST', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + describe('FIRST_VALUE', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + 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'] + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property', + BY: '@by' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by', 'DIALECT', DEFAULT_DIALECT] ); - }); + }); + - describe('FILTER', () => { + it('{ property: string }', () => { assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.FILTER, - expression: '@field != ""' - }] - }), - ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""'] + parseArgs(AGGREGATE, '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', 'DIALECT', DEFAULT_DIALECT] ); + }); }); - }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', '*', { - PARAMS: { - param: 'value' + it('with direction', () => { + assert.deepEqual( + parseArgs(AGGREGATE, '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', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', '*', { - DIALECT: 1 - }), - ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1'] - ); + it('RANDOM_SAMPLE', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'RANDOM_SAMPLE', + property: '@property', + sampleSize: 1 + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + describe('SORTBY', () => { + it('string', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'SORTBY', + BY: '@by' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT] + ); }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments('index', '*', { TIMEOUT: 10 }), - ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10'] - ); + it('Array', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'SORTBY', + BY: ['@1', '@2'] + }] + }), + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] + ); }); - }); - testUtils.testWithClient('client.ft.aggregate', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC + it('with MAX', () => { + assert.deepEqual( + parseArgs(AGGREGATE, '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', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + 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 - } - }) - ] - } + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'APPLY', + expression: '@field + 1', + AS: 'as' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + describe('LIMIT', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'LIMIT', + from: 0, + size: 1 + }] + }), + ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + describe('FILTER', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + STEPS: [{ + type: 'FILTER', + expression: '@field != ""' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""', 'DIALECT', DEFAULT_DIALECT] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + + it('with PARAMS', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + PARAMS: { + param: 'value' + } + }), + ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('with DIALECT', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { + DIALECT: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1'] + ); + }); + + it('with TIMEOUT', () => { + assert.deepEqual( + parseArgs(AGGREGATE, 'index', '*', { TIMEOUT: 10 }), + ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + 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..b2589a52a54 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -1,316 +1,333 @@ -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 { 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/dist/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; + +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { + parser.push('FT.AGGREGATE', index, query); + + return parseAggregateOptions(parser, 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 parseAggregateOptions(parser: CommandParser , options?: FtAggregateOptions) { + if (options?.VERBATIM) { + parser.push('VERBATIM'); + } + + if (options?.ADDSCORES) { + parser.push('ADDSCORES'); + } + + if (options?.LOAD) { + const args: Array = []; + + 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 - ); -} + parser.push('LOAD'); + parser.pushVariadicWithLength(args); + } + + if (options?.TIMEOUT !== undefined) { + parser.push('TIMEOUT', options.TIMEOUT.toString()); + } + + if (options?.STEPS) { + for (const step of options.STEPS) { + parser.push(step.type); + switch (step.type) { + case FT_AGGREGATE_STEPS.GROUPBY: + if (!step.properties) { + parser.push('0'); + } else { + parser.pushVariadicWithLength(step.properties); + } + + if (Array.isArray(step.REDUCE)) { + for (const reducer of step.REDUCE) { + parseGroupByReducer(parser, reducer); + } + } else { + parseGroupByReducer(parser, step.REDUCE); + } -export function pushAggregatehOptions( - args: RedisCommandArguments, - options?: AggregateOptions -): RedisCommandArguments { - if (options?.VERBATIM) { - args.push('VERBATIM'); - } + break; - if (options?.ADDSCORES) { - args.push('ADDSCORES'); - } + case FT_AGGREGATE_STEPS.SORTBY: + const args: Array = []; - 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.BY)) { + for (const by of step.BY) { + pushSortByProperty(args, by); } - }); - } + } else { + pushSortByProperty(args, step.BY); + } - 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; - } - } - } + if (step.MAX) { + args.push('MAX', step.MAX.toString()); + } - pushParamsArgs(args, options?.PARAMS); + parser.pushVariadicWithLength(args); - if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); - } + break; + + case FT_AGGREGATE_STEPS.APPLY: + parser.push(step.expression, 'AS', step.AS); + break; + + case FT_AGGREGATE_STEPS.LIMIT: + parser.push(step.from.toString(), step.size.toString()); + break; - if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + case FT_AGGREGATE_STEPS.FILTER: + parser.push(step.expression); + break; + } } + } - return args; + parseParamsArgument(parser, options?.PARAMS); + + if (options?.DIALECT) { + parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); + } } -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 parseGroupByReducer(parser: CommandParser, reducer: GroupByReducers) { + parser.push('REDUCE', reducer.type); + + switch (reducer.type) { + case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT: + parser.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: + parser.push('1', reducer.property); + break; + + case FT_AGGREGATE_GROUP_BY_REDUCERS.QUANTILE: + parser.push('2', reducer.property, reducer.quantile.toString()); + break; + + case FT_AGGREGATE_GROUP_BY_REDUCERS.FIRST_VALUE: { + const args: Array = [reducer.property]; + + 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); + parser.pushVariadicWithLength(args); + break; } -} -export type AggregateRawReply = [ - total: number, - ...results: Array> -]; + case FT_AGGREGATE_GROUP_BY_REDUCERS.RANDOM_SAMPLE: + parser.push('2', reducer.property, reducer.sampleSize.toString()); + break; + } -export interface AggregateReply { - total: number; - results: Array>; + if (reducer.AS) { + parser.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..0e89346c49f 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts @@ -1,37 +1,49 @@ -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'; +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( - transformArguments('index', '*'), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR'] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*'), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { + COUNT: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR', 'COUNT', '1'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('index', '*', { COUNT: 1 }), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1'] - ); - }); + it('with MAXIDLE', () => { + assert.deepEqual( + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { + MAXIDLE: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, '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..8dfca7169ef 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -1,44 +1,44 @@ -import { - AggregateOptions, - AggregateRawReply, - AggregateReply, - transformArguments as transformAggregateArguments, - transformReply as transformAggregateReply -} from './AGGREGATE'; +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 { FIRST_KEY_INDEX, IS_READ_ONLY } from './AGGREGATE'; - -interface AggregateWithCursorOptions extends AggregateOptions { - COUNT?: number; +export interface FtAggregateWithCursorOptions extends FtAggregateOptions { + COUNT?: number; + MAXIDLE?: 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()); - } - - return args; -} 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 { + IS_READ_ONLY: AGGREGATE.IS_READ_ONLY, + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { + AGGREGATE.parseCommand(parser, index, query, options); + parser.push('WITHCURSOR'); + + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); + } + + if(options?.MAXIDLE !== undefined) { + parser.push('MAXIDLE', options.MAXIDLE.toString()); + } + }, + 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..b8332aed6a6 100644 --- a/packages/search/lib/commands/ALIASADD.spec.ts +++ b/packages/search/lib/commands/ALIASADD.spec.ts @@ -1,11 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ALIASADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASADD', 'alias', 'index'] - ); - }); +describe('FT.ALIASADD', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ALIASADD, '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..c35e60bed4f 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -1,5 +1,11 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASADD', name, index]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 5255ba835db..19c2473f8cd 100644 --- a/packages/search/lib/commands/ALIASDEL.spec.ts +++ b/packages/search/lib/commands/ALIASDEL.spec.ts @@ -1,11 +1,26 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ALIASDEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASDEL', 'alias', 'index'] - ); - }); +describe('FT.ALIASDEL', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ALIASDEL, '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..9a2dbda4b9e 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -1,5 +1,11 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASDEL', name, index]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 79421b1a20d..f23af30229c 100644 --- a/packages/search/lib/commands/ALIASUPDATE.spec.ts +++ b/packages/search/lib/commands/ALIASUPDATE.spec.ts @@ -1,11 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ALIASUPDATE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASUPDATE', 'alias', 'index'] - ); - }); +describe('FT.ALIASUPDATE', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ALIASUPDATE, '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..3bd5ea92ba3 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -1,5 +1,11 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASUPDATE', name, index]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 e9724757ad7..c34f7e045d5 100644 --- a/packages/search/lib/commands/ALTER.spec.ts +++ b/packages/search/lib/commands/ALTER.spec.ts @@ -1,37 +1,36 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(ALTER, '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..4a68817bd2c 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -1,10 +1,13 @@ -import { RediSearchSchema, pushSchema } from '.'; +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 function transformArguments(index: string, schema: RediSearchSchema): Array { - const args = ['FT.ALTER', index, 'SCHEMA', 'ADD']; - pushSchema(args, schema); - - return args; -} - -export declare function transformReply(): 'OK'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 8614f443426..598a2a9ac41 100644 --- a/packages/search/lib/commands/CONFIG_GET.spec.ts +++ b/packages/search/lib/commands/CONFIG_GET.spec.ts @@ -1,25 +1,26 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT'), - ['FT.CONFIG', 'GET', 'TIMEOUT'] - ); - }); +describe('FT.CONFIG GET', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CONFIG_GET, '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..ae7a9e0c78d 100644 --- a/packages/search/lib/commands/CONFIG_GET.ts +++ b/packages/search/lib/commands/CONFIG_GET.ts @@ -1,16 +1,19 @@ -export function transformArguments(option: string) { - return ['FT.CONFIG', 'GET', option]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface ConfigGetReply { - [option: string]: string | null; -} - -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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, option: string) { + parser.push('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..c5922a28756 100644 --- a/packages/search/lib/commands/CONFIG_SET.spec.ts +++ b/packages/search/lib/commands/CONFIG_SET.spec.ts @@ -1,12 +1,56 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_SET'; - -describe('CONFIG SET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT', '500'), - ['FT.CONFIG', 'SET', 'TIMEOUT', '500'] - ); - }); +import CONFIG_SET from './CONFIG_SET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('FT.CONFIG SET', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CONFIG_SET, '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); + + 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/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts index 93b76d79edf..499b9525aa3 100644 --- a/packages/search/lib/commands/CONFIG_SET.ts +++ b/packages/search/lib/commands/CONFIG_SET.ts @@ -1,5 +1,15 @@ -export function transformArguments(option: string, value: string): Array { - return ['FT.CONFIG', 'SET', option, value]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 50c5c011c89..2c54d3d0235 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -1,490 +1,559 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('FT.CREATE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + WITHSUFFIXTRIE: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE'] + ); }); + }); + + it('NUMERIC', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC'] + ); + }); + + it('GEO', () => { + assert.deepEqual( + parseArgs(CREATE, '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'] + parseArgs(CREATE, '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'] + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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'] + parseArgs(CREATE, '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'] + parseArgs(CREATE, '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( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.GEOSHAPE, + COORD_SYSTEM: 'SPHERICAL' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL'] + ); + }); + }); + + it('with AS', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'as' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT'] + ); + }); + + describe('with SORTABLE', () => { + it('true', () => { + assert.deepEqual( + parseArgs(CREATE, '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( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + SORTABLE: 'UNF' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF'] + ); }); + }); + + it('with NOINDEX', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + NOINDEX: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX'] + ); + }); + + it('with INDEXMISSING', () => { + assert.deepEqual( + parseArgs(CREATE, '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( + parseArgs(CREATE, 'index', {}, { + ON: 'HASH' + }), + ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA'] + ); + }); + + describe('with PREFIX', () => { + it('string', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + PREFIX: 'prefix' + }), + ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA'] + ); + }); + + it('Array', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + PREFIX: ['1', '2'] + }), + ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA'] + ); + }); + }); + + it('with FILTER', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + FILTER: '@field != ""' + }), + ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA'] + ); + }); + + it('with LANGUAGE', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + LANGUAGE: REDISEARCH_LANGUAGE.ARABIC + }), + ['FT.CREATE', 'index', 'LANGUAGE', REDISEARCH_LANGUAGE.ARABIC, 'SCHEMA'] + ); + }); + + it('with LANGUAGE_FIELD', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + LANGUAGE_FIELD: '@field' + }), + ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA'] + ); + }); + + it('with SCORE', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + SCORE: 1 + }), + ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA'] + ); + }); + + it('with SCORE_FIELD', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + SCORE_FIELD: '@field' + }), + ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA'] + ); + }); + + it('with MAXTEXTFIELDS', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + MAXTEXTFIELDS: true + }), + ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA'] + ); + }); + + it('with TEMPORARY', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + TEMPORARY: 1 + }), + ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA'] + ); + }); + + it('with NOOFFSETS', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + NOOFFSETS: true + }), + ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA'] + ); + }); + + it('with NOHL', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + NOHL: true + }), + ['FT.CREATE', 'index', 'NOHL', 'SCHEMA'] + ); + }); + + it('with NOFIELDS', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + NOFIELDS: true + }), + ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA'] + ); + }); + + it('with NOFREQS', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + NOFREQS: true + }), + ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA'] + ); + }); + + it('with SKIPINITIALSCAN', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + SKIPINITIALSCAN: true + }), + ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA'] + ); + }); + + describe('with STOPWORDS', () => { + it('string', () => { + assert.deepEqual( + parseArgs(CREATE, 'index', {}, { + STOPWORDS: 'stopword' + }), + ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA'] ); - }, GLOBAL.SERVERS.OPEN); + }); + + it('Array', () => { + assert.deepEqual( + parseArgs(CREATE, '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); + + 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 21662c28d7d..5645a2b2dce 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -1,91 +1,361 @@ -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 { 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', + 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: 'FLOAT32' | 'FLOAT64' | 'BFLOAT16' | 'FLOAT16' | 'INT8' | 'UINT8'; + DIM: number; + DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE'; + INITIAL_CAP?: number; +} + +interface SchemaFlatVectorField extends SchemaVectorField { + ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['FLAT']; + BLOCK_SIZE?: number; +} + +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 parseCommonSchemaFieldOptions(parser: CommandParser, fieldOptions: SchemaCommonField) { + if (fieldOptions.SORTABLE) { + parser.push('SORTABLE'); + + if (fieldOptions.SORTABLE === 'UNF') { + parser.push('UNF'); + } + } + + if (fieldOptions.NOINDEX) { + parser.push('NOINDEX'); + } +} + +export function parseSchema(parser: CommandParser, schema: RediSearchSchema) { + for (const [field, fieldOptions] of Object.entries(schema)) { + parser.push(field); + + if (typeof fieldOptions === 'string') { + parser.push(fieldOptions); + continue; + } + + if (fieldOptions.AS) { + parser.push('AS', fieldOptions.AS); + } + + parser.push(fieldOptions.type); + + if (fieldOptions.INDEXMISSING) { + parser.push('INDEXMISSING'); + } + + switch (fieldOptions.type) { + case SCHEMA_FIELD_TYPE.TEXT: + if (fieldOptions.NOSTEM) { + parser.push('NOSTEM'); + } + + if (fieldOptions.WEIGHT) { + parser.push('WEIGHT', fieldOptions.WEIGHT.toString()); + } + + if (fieldOptions.PHONETIC) { + parser.push('PHONETIC', fieldOptions.PHONETIC); + } + + if (fieldOptions.WITHSUFFIXTRIE) { + parser.push('WITHSUFFIXTRIE'); + } + + if (fieldOptions.INDEXEMPTY) { + parser.push('INDEXEMPTY'); + } + + parseCommonSchemaFieldOptions(parser, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.NUMERIC: + case SCHEMA_FIELD_TYPE.GEO: + parseCommonSchemaFieldOptions(parser, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.TAG: + if (fieldOptions.SEPARATOR) { + parser.push('SEPARATOR', fieldOptions.SEPARATOR); + } + + if (fieldOptions.CASESENSITIVE) { + parser.push('CASESENSITIVE'); + } + + if (fieldOptions.WITHSUFFIXTRIE) { + parser.push('WITHSUFFIXTRIE'); + } + + if (fieldOptions.INDEXEMPTY) { + parser.push('INDEXEMPTY'); + } + + parseCommonSchemaFieldOptions(parser, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.VECTOR: + parser.push(fieldOptions.ALGORITHM); + + const args: Array = []; + + 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; + } + parser.pushVariadicWithLength(args); + + break; + + case SCHEMA_FIELD_TYPE.GEOSHAPE: + if (fieldOptions.COORD_SYSTEM !== undefined) { + parser.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); + } + + break; + } + } } -export function transformArguments(index: string, schema: RediSearchSchema, options?: CreateOptions): Array { - const args = ['FT.CREATE', index]; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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); } - pushOptionalVerdictArgument(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'); } - pushOptionalVerdictArgument(args, 'STOPWORDS', options?.STOPWORDS); - args.push('SCHEMA'); - pushSchema(args, schema); - - return args; -} - -export declare function transformReply(): 'OK'; + 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 d89725ef80d..230a5fd0feb 100644 --- a/packages/search/lib/commands/CURSOR_DEL.spec.ts +++ b/packages/search/lib/commands/CURSOR_DEL.spec.ts @@ -1,33 +1,33 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CURSOR DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index', 0), - ['FT.CURSOR', 'DEL', 'index', '0'] - ); - }); +describe('FT.CURSOR DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(CURSOR_DEL, '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..5f638ebb0ee 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 bb68e2b6396..42dca0c5756 100644 --- a/packages/search/lib/commands/CURSOR_READ.spec.ts +++ b/packages/search/lib/commands/CURSOR_READ.spec.ts @@ -1,45 +1,45 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(CURSOR_READ, '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( + parseArgs(CURSOR_READ, '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..e64070122d1 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -1,30 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { + parser.push('FT.CURSOR', 'READ', index, cursor.toString()); - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + if (options?.COUNT !== undefined) { + parser.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..4707db02dcf 100644 --- a/packages/search/lib/commands/DICTADD.spec.ts +++ b/packages/search/lib/commands/DICTADD.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DICTADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('dictionary', 'term'), - ['FT.DICTADD', 'dictionary', 'term'] - ); - }); +describe('FT.DICTADD', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(DICTADD, 'dictionary', 'term'), + ['FT.DICTADD', 'dictionary', 'term'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('dictionary', ['1', '2']), - ['FT.DICTADD', 'dictionary', '1', '2'] - ); - }); + it('Array', () => { + assert.deepEqual( + parseArgs(DICTADD, '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..2106775f854 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -1,8 +1,13 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/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 function transformArguments(dictionary: string, term: string | Array): RedisCommandArguments { - return pushVerdictArguments(['FT.DICTADD', dictionary], term); -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 5ffa6b6b84f..a9f997bdf38 100644 --- a/packages/search/lib/commands/DICTDEL.spec.ts +++ b/packages/search/lib/commands/DICTDEL.spec.ts @@ -1,28 +1,29 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DICTDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('dictionary', 'term'), - ['FT.DICTDEL', 'dictionary', 'term'] - ); - }); +describe('FT.DICTDEL', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(DICTDEL, 'dictionary', 'term'), + ['FT.DICTDEL', 'dictionary', 'term'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('dictionary', ['1', '2']), - ['FT.DICTDEL', 'dictionary', '1', '2'] - ); - }); + it('Array', () => { + assert.deepEqual( + parseArgs(DICTDEL, '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..988af1139e9 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -1,8 +1,13 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/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 function transformArguments(dictionary: string, term: string | Array): RedisCommandArguments { - return pushVerdictArguments(['FT.DICTDEL', dictionary], term); -} - -export declare function transformReply(): number; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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 9896fb9440d..1a3faa9dc9d 100644 --- a/packages/search/lib/commands/DICTDUMP.spec.ts +++ b/packages/search/lib/commands/DICTDUMP.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DICTDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('dictionary'), - ['FT.DICTDUMP', 'dictionary'] - ); - }); +describe('FT.DICTDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DICTDUMP, '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..3c223442ecb 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -1,5 +1,14 @@ -export function transformArguments(dictionary: string): Array { - return ['FT.DICTDUMP', dictionary]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, dictionary: RedisArgument) { + parser.push('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..f1f0b0efddb 100644 --- a/packages/search/lib/commands/DROPINDEX.spec.ts +++ b/packages/search/lib/commands/DROPINDEX.spec.ts @@ -1,33 +1,34 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DROPINDEX', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index'), - ['FT.DROPINDEX', 'index'] - ); - }); +describe('FT.DROPINDEX', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(DROPINDEX, 'index'), + ['FT.DROPINDEX', 'index'] + ); + }); - it('with DD', () => { - assert.deepEqual( - transformArguments('index', { DD: true }), - ['FT.DROPINDEX', 'index', 'DD'] - ); - }); + it('with DD', () => { + assert.deepEqual( + parseArgs(DROPINDEX, '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..407bdd031aa 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -1,15 +1,22 @@ -interface DropIndexOptions { - DD?: true; +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; } -export function transformArguments(index: string, options?: DropIndexOptions): Array { - const args = ['FT.DROPINDEX', index]; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument, options?: FtDropIndexOptions) { + parser.push('FT.DROPINDEX', index); if (options?.DD) { - args.push('DD'); + parser.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..d1691bc7c25 100644 --- a/packages/search/lib/commands/EXPLAIN.spec.ts +++ b/packages/search/lib/commands/EXPLAIN.spec.ts @@ -1,33 +1,48 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EXPLAIN'; +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'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('EXPLAIN', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('index', '*'), - ['FT.EXPLAIN', 'index', '*'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(EXPLAIN, 'index', '*'), + ['FT.EXPLAIN', 'index', '*', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', '*', { - PARAMS: { - param: 'value' - } - }), - ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value'] - ); - }); + it('with PARAMS', () => { + assert.deepEqual( + parseArgs(EXPLAIN, 'index', '*', { + PARAMS: { + param: 'value' + } + }), + ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', '*', { - DIALECT: 1 - }), - ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1'] - ); - }); + it('with DIALECT', () => { + assert.deepEqual( + parseArgs(EXPLAIN, '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..39a430f4371 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -1,26 +1,31 @@ -import { Params, pushParamsArgs } from "."; +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 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 { - const args = ['FT.EXPLAIN', index, query]; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + index: RedisArgument, + query: RedisArgument, + options?: FtExplainOptions + ) { + parser.push('FT.EXPLAIN', index, query); - pushParamsArgs(args, options?.PARAMS); + parseParamsArgument(parser, options?.PARAMS); if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } - - 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..1812b674094 100644 --- a/packages/search/lib/commands/EXPLAINCLI.spec.ts +++ b/packages/search/lib/commands/EXPLAINCLI.spec.ts @@ -1,11 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EXPLAINCLI'; +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( - transformArguments('index', '*'), - ['FT.EXPLAINCLI', 'index', '*'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(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 db97fb9c8da..4ef5fba88d6 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -1,7 +1,27 @@ -export const IS_READ_ONLY = true; +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 function transformArguments(index: string, query: string): Array { - return ['FT.EXPLAINCLI', index, query]; +export interface FtExplainCLIOptions { + DIALECT?: number; } -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts index e026b44e264..b52e99ab9b0 100644 --- a/packages/search/lib/commands/INFO.spec.ts +++ b/packages/search/lib/commands/INFO.spec.ts @@ -1,99 +1,216 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('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'); + assert.equal(ret.index_name, 'index'); + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7, 4, 2], [7, 4, 2]], '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); + + 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 + }, + } + ); - testUtils.testWithClient('client.ft.info', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }); - assert.deepEqual( - await client.ft.info('index'), - { - indexName: 'index', - indexOptions: [], - indexDefinition: 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 - } - })], - 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' - }, - cursorStats: { - globalIdle: 0, - globalTotal: 0, - indexCapacity: 128, - idnexTotal: 0 - }, - stopWords: undefined - } - ); - }, GLOBAL.SERVERS.OPEN); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 269d12d51cf..cee6ae683cd 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -1,167 +1,164 @@ -import { RedisCommandArgument } from '@redis/client/dist/lib/commands'; -import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; +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/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 function transformArguments(index: string): Array { - return ['FT.INFO', index]; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument) { + parser.push('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..dbb834ed7c2 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -1,46 +1,110 @@ -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'; +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( - transformArguments('index', 'query'), - ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query'] - ); - }); - - it('with options', () => { - assert.deepEqual( - transformArguments('index', 'query', { - LIMITED: true, - VERBATIM: true, - STEPS: [{ - type: AggregateSteps.SORTBY, - BY: '@by' - }] - }), - ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'SORTBY', '1', '@by'] - ); - }); + 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: SchemaFieldTypes.NUMERIC - }), - client.hSet('1', 'field', '1'), - client.hSet('2', 'field', '2') - ]); - - const res = await client.ft.profileAggregate('index', '*'); - 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, 2); + + 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, 2); + 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 b28e06ade91..94bb6984afa 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -1,29 +1,35 @@ -import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE'; -import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 const IS_READ_ONLY = true; - -export function transformArguments( +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, index: string, query: string, - options?: ProfileOptions & AggregateOptions -): Array { - const args = ['FT.PROFILE', index, 'AGGREGATE']; + options?: ProfileOptions & FtAggregateOptions + ) { + parser.push('FT.PROFILE', index, 'AGGREGATE'); if (options?.LIMITED) { - args.push('LIMITED'); + parser.push('LIMITED'); } - args.push('QUERY', query); - pushAggregatehOptions(args, options) - return args; -} - -type ProfileAggeregateRawReply = ProfileRawReply; + parser.push('QUERY', query); -export function transformReply(reply: ProfileAggeregateRawReply): ProfileReply { - return { - results: transformAggregateReply(reply[0]), - profile: transformProfile(reply[1]) - }; -} + 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 6d7c5adda1e..419b879d00a 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts @@ -1,41 +1,95 @@ -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'; +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( - transformArguments('index', 'query'), - ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query'] - ); - }); - - it('with options', () => { - assert.deepEqual( - transformArguments('index', 'query', { - LIMITED: true, - VERBATIM: true, - INKEYS: 'key' - }), - ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'INKEYS', '1', 'key'] - ); - }); + 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: SchemaFieldTypes.NUMERIC - }), - client.hSet('1', 'field', '1') - ]); - - const res = await client.ft.profileSearch('index', '*'); - 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 94fba8a6a54..b13dbebe996 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -1,29 +1,51 @@ -import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH'; -import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 const IS_READ_ONLY = true; +export type ProfileRawReplyResp2 = TuplesReply<[ + T, + ArrayReply +]>; -export function transformArguments( - index: string, - query: string, - options?: ProfileOptions & SearchOptions -): RedisCommandArguments { - let args: RedisCommandArguments = ['FT.PROFILE', index, 'SEARCH']; +type ProfileSearchResponseResp2 = ProfileRawReplyResp2; - if (options?.LIMITED) { - args.push('LIMITED'); - } +export interface ProfileReplyResp2 { + results: SearchReply | AggregateReply; + profile: ReplyUnion; +} - args.push('QUERY', query); - return pushSearchOptions(args, options); +export interface ProfileOptions { + LIMITED?: true; } -type ProfileSearchRawReply = ProfileRawReply; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + index: RedisArgument, + query: RedisArgument, + options?: ProfileOptions & FtSearchOptions + ) { + parser.push('FT.PROFILE', index, 'SEARCH'); -export function transformReply(reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply { - return { - results: transformSearchReply(reply[0], withoutDocuments), - profile: transformProfile(reply[1]) - }; -} + if (options?.LIMITED) { + parser.push('LIMITED'); + } + + parser.push('QUERY', query); + + parseSearchOptions(parser, options); + }, + transformReply: { + 2: (reply: UnwrapReply): ProfileReplyResp2 => { + return { + results: SEARCH.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/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index 931458b3a25..ab480808ffa 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -1,300 +1,330 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; -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( + parseArgs(SEARCH, 'index', 'query'), + ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with RETURN', () => { - assert.deepEqual( - transformArguments('index', 'query', { RETURN: 'return' }), - ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return'] - ); - }); + it('with VERBATIM', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + VERBATIM: true + }), + ['FT.SEARCH', 'index', 'query', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT] + ); + }); - describe('with SUMMARIZE', () => { - it('true', () => { - assert.deepEqual( - transformArguments('index', 'query', { SUMMARIZE: true }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE'] - ); - }); + it('with NOSTOPWORDS', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + NOSTOPWORDS: true + }), + ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + INKEYS: 'key' + }), + ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + INFIELDS: 'field' + }), + ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with FRAGS', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - FRAGS: 1 - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1'] - ); - }); + it('with RETURN', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + RETURN: 'return' + }), + ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: true + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: { + FIELDS: '@field' + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT] + ); }); - describe('with HIGHLIGHT', () => { - it('true', () => { - assert.deepEqual( - transformArguments('index', 'query', { HIGHLIGHT: true }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT'] - ); - }); + it('Array', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: { + FIELDS: ['@1', '@2'] + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: { + FRAGS: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: { + LEN: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SUMMARIZE: { + SEPARATOR: 'separator' + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); - it('with SLOP', () => { - assert.deepEqual( - transformArguments('index', 'query', { SLOP: 1 }), - ['FT.SEARCH', 'index', 'query', 'SLOP', '1'] - ); - }); + describe('with HIGHLIGHT', () => { + it('true', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + HIGHLIGHT: true + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with INORDER', () => { - assert.deepEqual( - transformArguments('index', 'query', { INORDER: true }), - ['FT.SEARCH', 'index', 'query', 'INORDER'] - ); + describe('with FIELDS', () => { + it('string', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + HIGHLIGHT: { + FIELDS: ['@field'] + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT] + ); }); - it('with LANGUAGE', () => { - assert.deepEqual( - transformArguments('index', 'query', { LANGUAGE: RedisSearchLanguages.ARABIC }), - ['FT.SEARCH', 'index', 'query', 'LANGUAGE', RedisSearchLanguages.ARABIC] - ); + it('Array', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + HIGHLIGHT: { + FIELDS: ['@1', '@2'] + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] + ); }); + }); - it('with EXPANDER', () => { - assert.deepEqual( - transformArguments('index', 'query', { EXPANDER: 'expender' }), - ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender'] - ); - }); + it('with TAGS', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + HIGHLIGHT: { + TAGS: { + open: 'open', + close: 'close' + } + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); - it('with SCORER', () => { - assert.deepEqual( - transformArguments('index', 'query', { SCORER: 'scorer' }), - ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer'] - ); - }); + it('with SLOP', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + SLOP: 1 + }), + ['FT.SEARCH', 'index', 'query', 'SLOP', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with SORTBY', () => { - assert.deepEqual( - transformArguments('index', 'query', { SORTBY: '@by' }), - ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by'] - ); - }); + it('with TIMEOUT', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + TIMEOUT: 1 + }), + ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + INORDER: true + }), + ['FT.SEARCH', 'index', 'query', 'INORDER', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', 'query', { - PARAMS: { - param: 'value' - } - }), - ['FT.SEARCH', 'index', 'query', 'PARAMS', '2', 'param', 'value'] - ); - }); + it('with LANGUAGE', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + LANGUAGE: 'Arabic' + }), + ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - DIALECT: 1 - }), - ['FT.SEARCH', 'index', 'query', 'DIALECT', '1'] - ); - }); + it('with EXPANDER', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + EXPANDER: 'expender' + }), + ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - TIMEOUT: 5 - }), - ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '5'] - ); - }); + it('with SCORER', () => { + assert.deepEqual( + parseArgs(SEARCH, 'index', 'query', { + SCORER: 'scorer' + }), + ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer', 'DIALECT', DEFAULT_DIALECT] + ); }); - 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( + parseArgs(SEARCH, 'index', 'query', { + SORTBY: '@by' + }), + ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, 'index', 'query', { + LIMIT: { + from: 0, + size: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, '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', 'DIALECT', DEFAULT_DIALECT] + ); + }); - 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( + parseArgs(SEARCH, '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..f48ac056784 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -1,109 +1,226 @@ -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 { 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; + +export function parseParamsArgument(parser: CommandParser, params?: FtSearchParams) { + if (params) { + parser.push('PARAMS'); + + const args: Array = []; + for (const key in params) { + if (!Object.hasOwn(params, key)) continue; + + const value = params[key]; + args.push( + key, + typeof value === 'number' ? value.toString() : value + ); + } + + parser.pushVariadicWithLength(args); + } +} + +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 parseSearchOptions(parser: CommandParser, options?: FtSearchOptions) { + if (options?.VERBATIM) { + parser.push('VERBATIM'); + } + + if (options?.NOSTOPWORDS) { + parser.push('NOSTOPWORDS'); + } + + parseOptionalVariadicArgument(parser, 'INKEYS', options?.INKEYS); + parseOptionalVariadicArgument(parser, 'INFIELDS', options?.INFIELDS); + parseOptionalVariadicArgument(parser, 'RETURN', options?.RETURN); + + if (options?.SUMMARIZE) { + parser.push('SUMMARIZE'); + + if (typeof options.SUMMARIZE === 'object') { + parseOptionalVariadicArgument(parser, 'FIELDS', options.SUMMARIZE.FIELDS); + + if (options.SUMMARIZE.FRAGS !== undefined) { + parser.push('FRAGS', options.SUMMARIZE.FRAGS.toString()); + } + + if (options.SUMMARIZE.LEN !== undefined) { + parser.push('LEN', options.SUMMARIZE.LEN.toString()); + } + + if (options.SUMMARIZE.SEPARATOR !== undefined) { + parser.push('SEPARATOR', options.SUMMARIZE.SEPARATOR); + } + } + } + + if (options?.HIGHLIGHT) { + parser.push('HIGHLIGHT'); + + if (typeof options.HIGHLIGHT === 'object') { + parseOptionalVariadicArgument(parser, 'FIELDS', options.HIGHLIGHT.FIELDS); + + if (options.HIGHLIGHT.TAGS) { + parser.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close); + } + } + } + + if (options?.SLOP !== undefined) { + parser.push('SLOP', options.SLOP.toString()); + } + + if (options?.TIMEOUT !== undefined) { + parser.push('TIMEOUT', options.TIMEOUT.toString()); + } + + if (options?.INORDER) { + parser.push('INORDER'); + } + + if (options?.LANGUAGE) { + parser.push('LANGUAGE', options.LANGUAGE); + } + + if (options?.EXPANDER) { + parser.push('EXPANDER', options.EXPANDER); + } + + if (options?.SCORER) { + parser.push('SCORER', options.SCORER); + } + + if (options?.SORTBY) { + parser.push('SORTBY'); + + if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) { + parser.push(options.SORTBY); + } else { + parser.push(options.SORTBY.BY); + + if (options.SORTBY.DIRECTION) { + parser.push(options.SORTBY.DIRECTION); + } + } + } + + if (options?.LIMIT) { + parser.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString()); + } + + parseParamsArgument(parser, options?.PARAMS); + + if (options?.DIALECT) { + parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); + } } -export type SearchRawReply = Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { + parser.push('FT.SEARCH', index, query); + + parseSearchOptions(parser, 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..cd37409b5bb 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts @@ -1,45 +1,36 @@ 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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; -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( + parseArgs(SEARCH_NOCONTENT, 'index', 'query'), + ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT, '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..a6968851acd 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -1,30 +1,26 @@ -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 { + NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND, + IS_READ_ONLY: SEARCH.IS_READ_ONLY, + parseCommand(...args: Parameters) { + SEARCH.parseCommand(...args); + args[0].push('NOCONTENT'); + }, + 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..482deed6a45 100644 --- a/packages/search/lib/commands/SPELLCHECK.spec.ts +++ b/packages/search/lib/commands/SPELLCHECK.spec.ts @@ -1,80 +1,81 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; -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( + parseArgs(SPELLCHECK, 'index', 'query'), + ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', DEFAULT_DIALECT] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - DIALECT: 1 - }), - ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1'] - ); - }); + it('with DISTANCE', () => { + assert.deepEqual( + parseArgs(SPELLCHECK, 'index', 'query', { + DISTANCE: 2 + }), + ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2', 'DIALECT', DEFAULT_DIALECT] + ); }); - 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( + parseArgs(SPELLCHECK, 'index', 'query', { + TERMS: { + mode: 'INCLUDE', + dictionary: 'dictionary' + } + }), + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary', 'DIALECT', DEFAULT_DIALECT] + ); + }); + it('multiple', () => { assert.deepEqual( - await client.ft.spellCheck('index', 'quer'), - [{ - term: 'quer', - suggestions: [{ - score: 1, - suggestion: 'query' - }] + parseArgs(SPELLCHECK, 'index', 'query', { + TERMS: [{ + mode: 'INCLUDE', + dictionary: 'include' + }, { + mode: 'EXCLUDE', + dictionary: 'exclude' }] + }), + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude', 'DIALECT', DEFAULT_DIALECT] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + + it('with DIALECT', () => { + assert.deepEqual( + parseArgs(SPELLCHECK, '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..3b909cdca32 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -1,62 +1,73 @@ -interface SpellCheckTerms { - mode: 'INCLUDE' | 'EXCLUDE'; - dictionary: string; +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'; + 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 { - const args = ['FT.SPELLCHECK', index, query]; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + 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); - } - } else { - pushTerms(args, options.TERMS); + if (Array.isArray(options.TERMS)) { + for (const term of options.TERMS) { + parseTerms(parser, term); } + } else { + parseTerms(parser, options.TERMS); + } } if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } + }, + 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; - return args; -} - -function pushTerms(args: Array, { mode, dictionary }: SpellCheckTerms): void { - args.push('TERMS', mode, dictionary); +function parseTerms(parser: CommandParser, { mode, dictionary }: Terms) { + parser.push('TERMS', mode, dictionary); } 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 - })) - })); -} diff --git a/packages/search/lib/commands/SUGADD.spec.ts b/packages/search/lib/commands/SUGADD.spec.ts index 23294eb4abd..2e0ce92edbc 100644 --- a/packages/search/lib/commands/SUGADD.spec.ts +++ b/packages/search/lib/commands/SUGADD.spec.ts @@ -1,35 +1,36 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(SUGADD, '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( + parseArgs(SUGADD, '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( + parseArgs(SUGADD, '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..34e5bccb7f1 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['FT.SUGADD', key, string, score.toString()]; +export default { + IS_READ_ONLY: true, + 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; -} - -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..21677f14213 100644 --- a/packages/search/lib/commands/SUGDEL.spec.ts +++ b/packages/search/lib/commands/SUGDEL.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SUGDEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'string'), - ['FT.SUGDEL', 'key', 'string'] - ); - }); +describe('FT.SUGDEL', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SUGDEL, '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..6bc99456d2e 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -1,5 +1,12 @@ -export function transformArguments(key: string, string: string): Array { - return ['FT.SUGDEL', key, string]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: true, + 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 c24c2ff0863..b82ea547782 100644 --- a/packages/search/lib/commands/SUGGET.spec.ts +++ b/packages/search/lib/commands/SUGGET.spec.ts @@ -1,46 +1,57 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGGET'; - -describe('SUGGET', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix'] - ); - }); - - it('with FUZZY', () => { - assert.deepEqual( - 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'] - ); - }); +import SUGGET from './SUGGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('FT.SUGGET', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(SUGGET, 'key', 'prefix'), + ['FT.SUGGET', 'key', 'prefix'] + ); + }); + + it('with FUZZY', () => { + assert.deepEqual( + parseArgs(SUGGET, 'key', 'prefix', { FUZZY: true }), + ['FT.SUGGET', 'key', 'prefix', 'FUZZY'] + ); }); - 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); - - assert.deepEqual( - await client.ft.sugGet('key', 'string'), - ['string'] - ); - }, GLOBAL.SERVERS.OPEN); + it('with MAX', () => { + assert.deepEqual( + parseArgs(SUGGET, 'key', 'prefix', { MAX: 10 }), + ['FT.SUGGET', 'key', 'prefix', 'MAX', '10'] + ); }); + }); + + describe('client.ft.sugGet', () => { + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGet('key', 'prefix'), + [] + ); + }, 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), + client.ft.sugGet('key', 's') + ]); + + 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..e8a3aecdab0 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { - const args = ['FT.SUGGET', key, prefix]; +export default { + IS_READ_ONLY: true, + 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) { - args.push('MAX', options.MAX.toString()); + if (options?.MAX !== undefined) { + parser.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..c01b87e2892 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts @@ -1,33 +1,43 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SUGGET WITHPAYLOADS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS'] - ); - }); +describe('FT.SUGGET WITHPAYLOADS', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SUGGET_WITHPAYLOADS, '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); + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithPayloads('key', 'prefix'), + [] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' }); + testUtils.testWithClientIfVersionWithinRange([[6], [7, 4, 0]], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithPayloads('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); - assert.deepEqual( - await client.ft.sugGetWithPayloads('key', 'string'), - [{ - suggestion: 'string', - payload: 'payload' - }] - ); - }, GLOBAL.SERVERS.OPEN); - }); + describe('with suggestions', () => { + 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(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..60bf5ee86d9 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -1,29 +1,29 @@ -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 { + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push('WITHPAYLOADS'); + }, + 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..50db89ffe99 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.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_WITHSCORES'; +import SUGGET_WITHSCORES from './SUGGET_WITHSCORES'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SUGGET WITHSCORES', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES'] - ); - }); +describe('FT.SUGGET WITHSCORES', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SUGGET_WITHSCORES, '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('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1); + testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithScores('key', 'prefix'), + [] + ); + }, GLOBAL.SERVERS.OPEN); - assert.deepEqual( - await client.ft.sugGetWithScores('key', 'string'), - [{ - suggestion: 'string', - score: 2147483648 - }] - ); - }, GLOBAL.SERVERS.OPEN); - }); + testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'],'with suggestions', async client => { + const [, reply] = await Promise.all([ + client.ft.sugAdd('key', 'string', 1), + client.ft.sugGetWithScores('key', 's') + ]); + + 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..060e59132db 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -1,29 +1,48 @@ -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 { + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push('WITHSCORES'); + }, + 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..96eb473159f 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts @@ -1,34 +1,37 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(SUGGET_WITHSCORES_WITHPAYLOADS, '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.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'), + [] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' }); + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], '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..07277420338 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -1,30 +1,54 @@ -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 { + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push( + 'WITHSCORES', + 'WITHPAYLOADS' + ); + }, + 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..d738f09042e 100644 --- a/packages/search/lib/commands/SUGLEN.spec.ts +++ b/packages/search/lib/commands/SUGLEN.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SUGLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['FT.SUGLEN', 'key'] - ); - }); +describe('FT.SUGLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SUGLEN, '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..a3f0fbe45ed 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 { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string): Array { - return ['FT.SUGLEN', key]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: true, + 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 472db54bcf8..88bf50cfb54 100644 --- a/packages/search/lib/commands/SYNDUMP.spec.ts +++ b/packages/search/lib/commands/SYNDUMP.spec.ts @@ -1,24 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('SYNDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index'), - ['FT.SYNDUMP', 'index'] - ); - }); +describe('FT.SYNDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(SYNDUMP, '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..5f454f96fe0 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -1,5 +1,23 @@ -export function transformArguments(index: string): Array { - return ['FT.SYNDUMP', index]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument) { + parser.push('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..f93e0599151 100644 --- a/packages/search/lib/commands/SYNUPDATE.spec.ts +++ b/packages/search/lib/commands/SYNUPDATE.spec.ts @@ -1,40 +1,43 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(SYNUPDATE, '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( + parseArgs(SYNUPDATE, '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( + parseArgs(SYNUPDATE, '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..3af735412ae 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -1,23 +1,28 @@ -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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'; -interface SynUpdateOptions { - SKIPINITIALSCAN?: true; +export interface FtSynUpdateOptions { + SKIPINITIALSCAN?: boolean; } -export function transformArguments( - index: string, - groupId: string, - terms: string | Array, - options?: SynUpdateOptions -): RedisCommandArguments { - const args = ['FT.SYNUPDATE', index, groupId]; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + index: RedisArgument, + groupId: RedisArgument, + terms: RedisVariadicArgument, + options?: FtSynUpdateOptions + ) { + parser.push('FT.SYNUPDATE', index, groupId); if (options?.SKIPINITIALSCAN) { - args.push('SKIPINITIALSCAN'); + parser.push('SKIPINITIALSCAN'); } - return pushVerdictArguments(args, terms); -} - -export declare function transformReply(): 'OK'; + 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 d59bfcfe3ea..f0d83c9f7ad 100644 --- a/packages/search/lib/commands/TAGVALS.spec.ts +++ b/packages/search/lib/commands/TAGVALS.spec.ts @@ -1,24 +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 './TAGVALS'; +import TAGVALS from './TAGVALS'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('TAGVALS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index', '@field'), - ['FT.TAGVALS', 'index', '@field'] - ); - }); +describe('FT.TAGVALS', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(TAGVALS, '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..0afddb247fd 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -1,5 +1,14 @@ -export function transformArguments(index: string, fieldName: string): Array { - return ['FT.TAGVALS', index, fieldName]; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) { + parser.push('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..dfe32f2e29d 100644 --- a/packages/search/lib/commands/_LIST.spec.ts +++ b/packages/search/lib/commands/_LIST.spec.ts @@ -1,19 +1,20 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('_LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FT._LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(_LIST), + ['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..c1ca8cc2ee5 100644 --- a/packages/search/lib/commands/_LIST.ts +++ b/packages/search/lib/commands/_LIST.ts @@ -1,5 +1,14 @@ -export function transformArguments(): Array { - return ['FT._LIST']; -} +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser) { + parser.push('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..7aa3f061bf7 100644 --- a/packages/search/lib/commands/index.ts +++ b/packages/search/lib/commands/index.ts @@ -1,690 +1,117 @@ -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, + /** + * @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, + 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/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'; diff --git a/packages/search/lib/index.ts b/packages/search/lib/index.ts index 0f84c11466f..9bcfb91b956 100644 --- a/packages/search/lib/index.ts +++ b/packages/search/lib/index.ts @@ -1,5 +1,21 @@ -export { default } from './commands'; +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 { 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/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 9e0af209103..7264b1b6b12 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: 'redislabs/redisearch', - dockerImageVersionArgument: 'redisearch-version', - defaultDockerVersion: '2.4.9' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisearch.so'], - 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 aaf9bc50f11..ba1fa2a74be 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,30 +1,25 @@ { "name": "@redis/search", - "version": "1.2.0", + "version": "5.0.1", "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'", + "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^5.0.1" }, "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/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 a7e1c610eee..3814a80923b 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -1,260 +1,426 @@ -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'; -const execAsync = promisify(exec); +import { RedisClusterClientOptions } from '@redis/client/dist/lib/cluster'; +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 { 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); 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; +interface RedisServerDockerConfig { + image: string; + version: string; } -export interface RedisServerDocker { - port: number; - dockerId: string; +interface SentinelConfig { + mode: "sentinel"; + mounts: Array; + port: number; } -// ".." 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(' ')}"` + - ')' - ); +interface ServerConfig { + mode: "server"; +} - if (!stdout) { - throw new Error(`docker run error - ${stderr}`); - } +export type RedisServerDockerOptions = RedisServerDockerConfig & (SentinelConfig | ServerConfig) + +export interface RedisServerDocker { + port: number; + dockerId: string; +} - while (await isPortAvailable(port)) { - await promiseTimeout(50); +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', + '--init', + '-e', `PORT=${portStr}` + ]; + + if (options.mode == "sentinel") { + options.mounts.forEach(mount => { + dockerArgs.push('-v', mount); + }); + } + + dockerArgs.push( + '-d', + '--network', 'host', + `${options.image}:${options.version}` + ); + + if (serverArguments.length > 0) { + for (let i = 0; i < serverArguments.length; i++) { + dockerArgs.push(serverArguments[i]) } + } - return { - port, - dockerId: stdout.trim() - }; -} + console.log(`[Docker] Spawning Redis container - Image: ${options.image}:${options.version}, Port: ${port}, Mode: ${options.mode}`); + + const { stdout, stderr } = await execAsync('docker', dockerArgs); + + if (!stdout) { + throw new Error(`docker run error - ${stderr}`); + } + while (await isPortAvailable(port)) { + await setTimeout(50); + } + + 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; - } +export function spawnRedisServer(dockerConfig: RedisServerDockerOptions, 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 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; +export type RedisClusterDockersConfig = RedisServerDockerOptions & { + numberOfMasters?: number; + numberOfReplicas?: number; } async function spawnRedisClusterNodeDockers( - dockersConfig: RedisClusterDockersConfig, - serverArguments: Array, - fromSlot: number, - toSlot: number + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array, + fromSlot: number, + toSlot: number, + clientConfig?: Partial ) { - 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, + clientConfig + ); + + 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' + ], 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); - await master.client.clusterAddSlots(range); + while ((await replica.client.clusterSlots()).length === 0) { + await setTimeout(25); + } - 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: RedisServerDockerOptions, + serverArguments: Array, + clientConfig?: Partial ) { - 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 + }, + ...clientConfig + }); + + await client.connect(); + + return { + docker, + client + }; } const SLOTS = 16384; async function spawnRedisClusterDockers( - dockersConfig: RedisClusterDockersConfig, - serverArguments: Array + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array, + clientConfig?: Partial ): 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, + clientConfig + ) + ); + } - 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); + } - return nodes.map(({ docker }) => docker); + client.destroy(); + }) + ); + + 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; +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, clientConfig); + + 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)) + ); + }) + ); +}); + + +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 }); } - const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments); - RUNNING_CLUSTERS.set(serverArguments, dockersPromise); - return dockersPromise; + return sentinelNodes; } after(() => { - return Promise.all( - [...RUNNING_CLUSTERS.values()].map(async dockersPromise => { - return Promise.all( - (await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId)) - ); - }) - ); + 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.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 b9195c5717a..8ed85bf6e3e 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -1,226 +1,544 @@ -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 { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; +import { + RedisModules, + RedisFunctions, + RedisScripts, + RespVersions, + TypeMapping, + // CommandPolicies, + createClient, + createSentinel, + RedisClientOptions, + RedisClientType, + RedisSentinelOptions, + RedisSentinelType, + RedisPoolOptions, + RedisClientPoolType, + createClientPool, + createCluster, + RedisClusterOptions, + RedisClusterType +} from '@redis/client/index'; +import { RedisNode } from '@redis/client/lib/sentinel/types' +import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; + interface TestUtilsConfig { - dockerImageName: string; - dockerImageVersionArgument: string; - defaultDockerVersion?: string; -} + /** + * 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 { - minimumDockerVersion?: Array; + serverArguments: Array; + minimumDockerVersion?: Array; + skipTest?: boolean; } 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 SentinelTestOptions< + 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; + 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, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends CommonTestOptions { + 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]; + + // Match complete version number patterns + const versionMatch = version.match(/(^|\-)\d+(\.\d+)*($|\-)/); + if (!versionMatch) { + throw new TypeError(`${version} is not a valid redis version`); } - readonly #VERSION_NUMBERS: Array; - readonly #DOCKER_IMAGE: RedisServerDockerConfig; + // Extract just the numbers and dots between first and last dash (or start/end) + const versionNumbers = versionMatch[0].replace(/^\-|\-$/g, ''); - 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 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, { + 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: RedisServerDockerOptions; - 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({ string, numbers }: Version, dockerImageName: string) { + this.#VERSION_NUMBERS = numbers; + this.#DOCKER_IMAGE = { + image: dockerImageName, + version: string, + mode: "server" + }; + } + + /** + * 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; + return TestUtils.compareVersions(this.#VERSION_NUMBERS, minimumVersion) >= 0; + } + + isVersionGreaterThanHook(minimumVersion: Array | undefined): void { + const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this); + const versionNumber = this.#VERSION_NUMBERS.join('.'); + const minimumVersionString = minimumVersion?.join('.'); + before(function () { + 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 + } - return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex]; + /** + * 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; } - isVersionGreaterThanHook(minimumVersion: Array | undefined): void { - const isVersionGreaterThan = this.isVersionGreaterThan.bind(this); - before(function () { - if (!isVersionGreaterThan(minimumVersion)) { - return this.skip(); - } - }); + return 0; + } + + 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 (options.skipTest) return this.skip(); + if (!dockerPromise) return this.skip(); + + const client = createClient({ + ...options.clientOptions, + socket: { + ...options.clientOptions?.socket, + 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(); + 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; - try { - await client.flushAll(); - await fn(client); - } finally { - if (client.isOpen) { - await client.flushAll(); - await client.disconnect(); - } - } - }); + 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); + } - 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(); - } - }) - ); + 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 = {}, + 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'}`) } + } + + 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 = {}, + 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); - 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; - }); + dockerPromise = spawnRedisServer(dockerImage, options.serverArguments); + return dockerPromise; + }); + } + + it(title, async function () { + if (options.skipTest) return this.skip(); + if (!dockerPromise) return this.skip(); + + const pool = createClientPool({ + ...options.clientOptions, + socket: { + ...options.clientOptions?.socket, + port: (await dockerPromise).port + } + }, options.poolOptions); + + await pool.connect(); + + 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, + options.clusterConfiguration?.defaults); + 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..f7373f6add1 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,24 +1,16 @@ { "name": "@redis/test-utils", "private": true, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "scripts": { - "build": "tsc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.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..055d2246d8b 100644 --- a/packages/time-series/lib/commands/ADD.spec.ts +++ b/packages/time-series/lib/commands/ADD.spec.ts @@ -1,90 +1,94 @@ -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 '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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( + parseArgs(ADD, '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..e7626d227da 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -1,55 +1,58 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; import { - transformTimestampArgument, - pushRetentionArgument, - TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, - TimeSeriesDuplicatePolicies, - Labels, - pushLabelsArgument, - Timestamp, - pushIgnoreArgument, + transformTimestampArgument, + parseRetentionArgument, + TimeSeriesEncoding, + parseEncodingArgument, + parseChunkSizeArgument, + TimeSeriesDuplicatePolicies, + Labels, + parseLabelsArgument, + Timestamp, + parseIgnoreArgument } 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 default { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + key: RedisArgument, + timestamp: Timestamp, + value: number, + options?: TsAddOptions + ) { + parser.push('TS.ADD'); + parser.pushKey(key); + parser.push(transformTimestampArgument(timestamp), value.toString()); -export function transformArguments(key: string, timestamp: Timestamp, value: number, options?: AddOptions): Array { - const args = [ - 'TS.ADD', - key, - transformTimestampArgument(timestamp), - value.toString() - ]; + parseRetentionArgument(parser, options?.RETENTION); - pushRetentionArgument(args, options?.RETENTION); + parseEncodingArgument(parser, options?.ENCODING); - pushEncodingArgument(args, 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); - - pushIgnoreArgument(args, options?.IGNORE); - - return args; -} + parseLabelsArgument(parser, options?.LABELS); -export declare function transformReply(): number; + 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 7add3eeec3a..560d9ffde2c 100644 --- a/packages/time-series/lib/commands/ALTER.spec.ts +++ b/packages/time-series/lib/commands/ALTER.spec.ts @@ -1,82 +1,86 @@ -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 '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('ALTER', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.ALTER', 'key'] - ); - }); +describe('TS.ALTER', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(ALTER, 'key'), + ['TS.ALTER', 'key'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', { - RETENTION: 1 - }), - ['TS.ALTER', 'key', 'RETENTION', '1'] - ); - }); + it('with RETENTION', () => { + assert.deepEqual( + parseArgs(ALTER, '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( + parseArgs(ALTER, '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( + parseArgs(ALTER, '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( + parseArgs(ALTER, '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( + parseArgs(ALTER, '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( + parseArgs(ALTER, '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..f7f6948da71 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,30 +1,25 @@ -import { pushRetentionArgument, Labels, pushLabelsArgument, TimeSeriesDuplicatePolicies, pushChunkSizeArgument, pushDuplicatePolicy, pushIgnoreArgument } from '.'; -import { TsIgnoreOptions } from './ADD'; +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 '.'; -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 default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsAlterOptions) { + parser.push('TS.ALTER'); + parser.pushKey(key); -export function transformArguments(key: string, options?: AlterOptions): Array { - const args = ['TS.ALTER', key]; + parseRetentionArgument(parser, options?.RETENTION); - pushRetentionArgument(args, options?.RETENTION); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY); - pushDuplicatePolicy(args, options?.DUPLICATE_POLICY); + parseLabelsArgument(parser, options?.LABELS); - pushLabelsArgument(args, options?.LABELS); - - pushIgnoreArgument(args, options?.IGNORE); - - return args; -} - -export declare function transformReply(): 'OK'; + parseIgnoreArgument(parser, options?.IGNORE); + }, + 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 eb7a1c6a637..795b59b880d 100644 --- a/packages/time-series/lib/commands/CREATE.spec.ts +++ b/packages/time-series/lib/commands/CREATE.spec.ts @@ -1,90 +1,94 @@ -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 '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('CREATE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.CREATE', 'key'] - ); - }); +describe('TS.CREATE', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(CREATE, 'key'), + ['TS.CREATE', 'key'] + ); + }); + + it('with RETENTION', () => { + assert.deepEqual( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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( + parseArgs(CREATE, '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..39f35c06ed7 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -1,43 +1,44 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { - pushRetentionArgument, - TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, - TimeSeriesDuplicatePolicies, - Labels, - pushLabelsArgument, - pushDuplicatePolicy, - pushIgnoreArgument + parseRetentionArgument, + TimeSeriesEncoding, + parseEncodingArgument, + parseChunkSizeArgument, + TimeSeriesDuplicatePolicies, + parseDuplicatePolicy, + Labels, + parseLabelsArgument, + parseIgnoreArgument } 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 { - const args = ['TS.CREATE', key]; - - pushRetentionArgument(args, options?.RETENTION); +export default { + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsCreateOptions) { + parser.push('TS.CREATE'); + parser.pushKey(key); - pushEncodingArgument(args, options?.ENCODING); + parseRetentionArgument(parser, options?.RETENTION); - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseEncodingArgument(parser, options?.ENCODING); - pushDuplicatePolicy(args, options?.DUPLICATE_POLICY); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); - pushLabelsArgument(args, options?.LABELS); + parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY); - pushIgnoreArgument(args, options?.IGNORE); - - return args; -} + parseLabelsArgument(parser, options?.LABELS); -export declare function transformReply(): 'OK'; + 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 65457898181..da26bf458e2 100644 --- a/packages/time-series/lib/commands/CREATERULE.spec.ts +++ b/packages/time-series/lib/commands/CREATERULE.spec.ts @@ -1,34 +1,32 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(CREATERULE, '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( + parseArgs(CREATERULE, '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..1e4f38c6ee6 100644 --- a/packages/time-series/lib/commands/CREATERULE.ts +++ b/packages/time-series/lib/commands/CREATERULE.ts @@ -1,28 +1,41 @@ -import { TimeSeriesAggregationType } from '.'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +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 { + IS_READ_ONLY: false, + parseCommand( + parser: CommandParser, + sourceKey: RedisArgument, + destinationKey: RedisArgument, aggregationType: TimeSeriesAggregationType, bucketDuration: number, alignTimestamp?: number -): Array { - 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) { - args.push(alignTimestamp.toString()); + if (alignTimestamp !== undefined) { + parser.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..b272ed1614d 100644 --- a/packages/time-series/lib/commands/DECRBY.spec.ts +++ b/packages/time-series/lib/commands/DECRBY.spec.ts @@ -1,81 +1,93 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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( + parseArgs(DECRBY, '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..8ff09d926c0 100644 --- a/packages/time-series/lib/commands/DECRBY.ts +++ b/packages/time-series/lib/commands/DECRBY.ts @@ -1,10 +1,13 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { IncrDecrOptions, transformIncrDecrArguments } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import INCRBY, { parseIncrByArguments } from './INCRBY'; -export const FIRST_KEY_INDEX = 1; +export default { + IS_READ_ONLY: INCRBY.IS_READ_ONLY, + parseCommand(...args: Parameters) { + const parser = args[0]; -export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments { - return transformIncrDecrArguments('TS.DECRBY', key, value, options); -} - -export declare function transformReply(): number; + 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 0fc4b465807..07d29ca095e 100644 --- a/packages/time-series/lib/commands/DEL.spec.ts +++ b/packages/time-series/lib/commands/DEL.spec.ts @@ -1,21 +1,22 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['TS.DEL', 'key', '-', '+'] - ); - }); +describe('TS.DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DEL, '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..fc96c989b18 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -1,15 +1,13 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; 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 { - return [ - 'TS.DEL', - key, - transformTimestampArgument(fromTimestamp), - transformTimestampArgument(toTimestamp) - ]; -} - -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + 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 9364bea711c..d7a19a8eaa1 100644 --- a/packages/time-series/lib/commands/DELETERULE.spec.ts +++ b/packages/time-series/lib/commands/DELETERULE.spec.ts @@ -1,26 +1,25 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('DELETERULE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['TS.DELETERULE', 'source', 'destination'] - ); - }); +describe('TS.DELETERULE', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DELETERULE, '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..b4e47a0fba6 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -1,11 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(sourceKey: string, destinationKey: string): Array { - return [ - 'TS.DELETERULE', - sourceKey, - destinationKey - ]; -} - -export declare function transformReply(): 'OK'; +export default { + IS_READ_ONLY: false, + 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 29634cd775a..836a1b638af 100644 --- a/packages/time-series/lib/commands/GET.spec.ts +++ b/packages/time-series/lib/commands/GET.spec.ts @@ -1,46 +1,47 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('GET', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.GET', 'key'] - ); - }); +describe('TS.GET', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(GET, 'key'), + ['TS.GET', 'key'] + ); + }); - it('with LATEST', () => { - assert.deepEqual( - transformArguments('key', { - LATEST: true - }), - ['TS.GET', 'key', 'LATEST'] - ); - }); + it('with LATEST', () => { + assert.deepEqual( + parseArgs(GET, '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..c1bb2c1c749 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -1,20 +1,34 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushLatestArgument, SampleRawReply, SampleReply, transformSampleReply } from '.'; +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 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; - - return transformSampleReply(reply); -} +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsGetOptions) { + parser.push('TS.GET'); + parser.pushKey(key); + + if (options?.LATEST) { + parser.push('LATEST'); + } + }, + 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..5d005952b30 100644 --- a/packages/time-series/lib/commands/INCRBY.spec.ts +++ b/packages/time-series/lib/commands/INCRBY.spec.ts @@ -1,91 +1,103 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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( + parseArgs(INCRBY, '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..e62ec42690a 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 { 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'; -export const FIRST_KEY_INDEX = 1; +export interface TsIncrByOptions { + TIMESTAMP?: Timestamp; + RETENTION?: number; + UNCOMPRESSED?: boolean; + CHUNK_SIZE?: number; + LABELS?: Labels; + IGNORE?: TsIgnoreOptions; +} + +export function parseIncrByArguments( + parser: CommandParser, + key: RedisArgument, + value: number, + options?: TsIncrByOptions +) { + parser.pushKey(key); + parser.push(value.toString()); + + if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) { + parser.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); + } + + parseRetentionArgument(parser, options?.RETENTION); -export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments { - return transformIncrDecrArguments('TS.INCRBY', key, value, options); + if (options?.UNCOMPRESSED) { + parser.push('UNCOMPRESSED'); + } + + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); + + parseLabelsArgument(parser, options?.LABELS); + + parseIgnoreArgument(parser, options?.IGNORE); } -export declare function transformReply(): number; +export default { + IS_READ_ONLY: false, + 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 c02cdd6da4d..73b9d8dc930 100644 --- a/packages/time-series/lib/commands/INFO.spec.ts +++ b/packages/time-series/lib/commands/INFO.spec.ts @@ -1,12 +1,14 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('INFO', () => { +describe('TS.INFO', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key'), + parseArgs(INFO, 'key'), ['TS.INFO', 'key'] ); }); @@ -15,14 +17,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..fe0e49e095a 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -1,82 +1,129 @@ -import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.'; +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/dist/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 { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: string) { + parser.push('TS.INFO'); + parser.pushKey(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; diff --git a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts index 666689f5194..063b9126550 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts @@ -1,30 +1,32 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -describe('INFO_DEBUG', () => { +describe('TS.INFO_DEBUG', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key'), + parseArgs(INFO_DEBUG, '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..89d66a36ef8 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -1,57 +1,77 @@ -import { - transformArguments as transformInfoArguments, - InfoRawReply, - InfoReply, - transformReply as transformInfoReply -} from './INFO'; - -export { IS_READ_ONLY, FIRST_KEY_INDEX } from './INFO'; - -export function transformArguments(key: string): Array { - const args = transformInfoArguments(key); - args.push('DEBUG'); - return args; -} +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<[ + '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 { + IS_READ_ONLY: INFO.IS_READ_ONLY, + parseCommand(parser: CommandParser, key: string) { + INFO.parseCommand(parser, key); + parser.push('DEBUG'); + }, + 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; diff --git a/packages/time-series/lib/commands/MADD.spec.ts b/packages/time-series/lib/commands/MADD.spec.ts index eed014f2b14..8bf8e27fdb3 100644 --- a/packages/time-series/lib/commands/MADD.spec.ts +++ b/packages/time-series/lib/commands/MADD.spec.ts @@ -1,39 +1,42 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MADD, [{ + 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..5af94d6d497 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -1,25 +1,22 @@ +import { CommandParser } from '@redis/client/dist/lib/client/parser'; 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 { - const args = ['TS.MADD']; +export default { + IS_READ_ONLY: false, + 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; -} - -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..ba2e571be49 100644 --- a/packages/time-series/lib/commands/MGET.spec.ts +++ b/packages/time-series/lib/commands/MGET.spec.ts @@ -1,40 +1,46 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MGET, '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( + parseArgs(MGET, '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..fa4e3fc63d6 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 { 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/dist/lib/commands/generic-transformers'; -export const IS_READ_ONLY = true; +export interface TsMGetOptions { + LATEST?: boolean; +} -export interface MGetOptions { - LATEST?: boolean; +export function parseLatestArgument(parser: CommandParser, latest?: boolean) { + if (latest) { + parser.push('LATEST'); + } } -export function transformArguments(filter: Filter, options?: MGetOptions): RedisCommandArguments { - const args = pushLatestArgument(['TS.MGET'], options?.LATEST); - return pushFilterArgument(args, filter); +export function parseFilterArgument(parser: CommandParser, filter: RedisVariadicArgument) { + parser.push('FILTER'); + parser.pushVariadic(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 { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) { + parser.push('TS.MGET'); + parseLatestArgument(parser, options?.LATEST); + parseFilterArgument(parser, 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..d79c463fc7d --- /dev/null +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts @@ -0,0 +1,47 @@ +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( + parseArgs(MGET_SELECTED_LABELS, '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..e93b517f80a --- /dev/null +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -0,0 +1,17 @@ +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'; + +export default { + IS_READ_ONLY: true, + 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 55fcfde409d..33fc5308444 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts @@ -1,39 +1,42 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MGET_WITHLABELS, '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..38b8442db31 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -1,37 +1,62 @@ -import { - SelectedLabels, - pushWithLabelsArgument, - Labels, - transformLablesReply, - transformSampleReply, - Filter, - pushFilterArgument -} from '.'; -import { MGetOptions, MGetRawReply, MGetReply } from './MGET'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +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 '.'; -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 { + IS_READ_ONLY: true, + 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 4228cc06fb7..94c8e72983a 100644 --- a/packages/time-series/lib/commands/MRANGE.spec.ts +++ b/packages/time-series/lib/commands/MRANGE.spec.ts @@ -1,50 +1,63 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MRANGE, '-', '+', '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..95fa5297bdd 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,21 +1,61 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.'; +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'; -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 ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, - filters: Filter, - options?: MRangeOptions -): RedisCommandArguments { - return pushMRangeArguments( - ['TS.MRANGE'], - fromTimestamp, - toTimestamp, - filters, - options + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments( + parser, + fromTimestamp, + toTimestamp, + options ); + + parseFilterArgument(parser, filter); + }; } -export { transformMRangeReply as transformReply } from '.'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand: 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..bcdde20fe98 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts @@ -0,0 +1,67 @@ +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( + parseArgs(MRANGE_GROUPBY, '-', '+', '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..5ccd61b2a26 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -0,0 +1,103 @@ +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'; + +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 parseGroupByArguments(parser: CommandParser, groupBy: TsMRangeGroupBy) { + parser.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 ( + parser: CommandParser, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments(parser, fromTimestamp, toTimestamp, options) + + parseFilterArgument(parser, filter); + + parseGroupByArguments(parser, groupBy); + }; +} + +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 { + IS_READ_ONLY: true, + parseCommand: 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..92680dea375 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts @@ -0,0 +1,73 @@ +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( + parseArgs(MRANGE_SELECTED_LABELS, '-', '+', '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..643b57a67e7 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -0,0 +1,72 @@ +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'; + +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 ( + parser: CommandParser, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + selectedLabels: RedisVariadicArgument, + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments( + parser, + fromTimestamp, + toTimestamp, + options + ); + + parseSelectedLabelsArguments(parser, selectedLabels); + + parseFilterArgument(parser, filter); + }; +} + +export default { + IS_READ_ONLY: true, + parseCommand: 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..4e5b2b47094 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -0,0 +1,81 @@ +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.MRANGE_SELECTED_LABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MRANGE_SELECTED_LABELS_GROUPBY, '-', '+', '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..c5cf1ef56c5 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -0,0 +1,63 @@ +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'; +import { parseFilterArgument } 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 ( + parser: CommandParser, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + selectedLabels: RedisVariadicArgument, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments( + parser, + fromTimestamp, + toTimestamp, + options + ); + + parseSelectedLabelsArguments(parser, selectedLabels); + + parseFilterArgument(parser, filter); + + parseGroupByArguments(parser, groupBy); + }; +} + +export default { + IS_READ_ONLY: true, + parseCommand: 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..eab2e1fadbe 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts @@ -1,52 +1,69 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MRANGE_WITHLABELS, '-', '+', '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..19641596a67 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,21 +1,81 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments } from '.'; +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'; -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 ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, - filters: string | Array, - options?: MRangeWithLabelsOptions -): RedisCommandArguments { - return pushMRangeWithLabelsArguments( - ['TS.MRANGE'], - fromTimestamp, - toTimestamp, - filters, - options + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments( + parser, + fromTimestamp, + toTimestamp, + options ); + + parser.push('WITHLABELS'); + + parseFilterArgument(parser, filter); + }; } -export { transformMRangeWithLabelsReply as transformReply } from '.'; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand: 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..4a8b8fe707f --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts @@ -0,0 +1,78 @@ +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.MRANGE_WITHLABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MRANGE_WITHLABELS_GROUPBY, '-', '+', '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..ff0065e22b7 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -0,0 +1,79 @@ +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'; +import { parseFilterArgument } 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 ( + parser: CommandParser, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + parser.push(command); + parseRangeArguments( + parser, + fromTimestamp, + toTimestamp, + options + ); + + parser.push('WITHLABELS'); + + parseFilterArgument(parser, filter); + + parseGroupByArguments(parser, groupBy); + }; +} + +export default { + IS_READ_ONLY: true, + parseCommand: 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..09051103f8b 100644 --- a/packages/time-series/lib/commands/MREVRANGE.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE.spec.ts @@ -1,50 +1,63 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MREVRANGE, '-', '+', '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..99d3123dd27 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 { + NOT_KEYED_COMMAND: MRANGE.NOT_KEYED_COMMAND, + IS_READ_ONLY: MRANGE.IS_READ_ONLY, + 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 new file mode 100644 index 00000000000..d32d675ad0a --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts @@ -0,0 +1,68 @@ +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.MREVRANGE_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MREVRANGE_GROUPBY, '-', '+', '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..4afcd113505 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -0,0 +1,8 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY'; + +export default { + IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY, + 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 new file mode 100644 index 00000000000..f68e34727c2 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts @@ -0,0 +1,74 @@ +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( + parseArgs(MREVRANGE_SELECTED_LABELS, '-', '+', '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..10e00fc7a29 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -0,0 +1,8 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS'; + +export default { + IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY, + 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 new file mode 100644 index 00000000000..444bb2f3d24 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -0,0 +1,81 @@ +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.MREVRANGE_SELECTED_LABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MREVRANGE_SELECTED_LABELS_GROUPBY, '-', '+', '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..b000c04c183 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -0,0 +1,8 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY'; + +export default { + IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY, + 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 7e80e965d4e..da43a715f2e 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts @@ -1,52 +1,69 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(MREVRANGE_WITHLABELS, '-', '+', '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..6cde143c422 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 { + NOT_KEYED_COMMAND: MRANGE_WITHLABELS.NOT_KEYED_COMMAND, + IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY, + 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 new file mode 100644 index 00000000000..f4e6df9f0c6 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts @@ -0,0 +1,78 @@ +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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.MREVRANGE_WITHLABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(MREVRANGE_WITHLABELS_GROUPBY, '-', '+', '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..4727112b974 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -0,0 +1,8 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY'; + +export default { + IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY, + 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 010c5c8f639..2f3f5617fb3 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.spec.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.spec.ts @@ -1,34 +1,35 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(QUERYINDEX, '*'), + ['TS.QUERYINDEX', '*'] + ); }); - testUtils.testWithClient('client.ts.queryIndex', async client => { - await client.ts.create('key', { - LABELS: { - label: 'value' - } - }); + it('multiple filters', () => { + assert.deepEqual( + parseArgs(QUERYINDEX, ['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..1b53e84b7a3 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -1,11 +1,16 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { Filter } from '.'; +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 const IS_READ_ONLY = true; - -export function transformArguments(filter: Filter): RedisCommandArguments { - return pushVerdictArguments(['TS.QUERYINDEX'], filter); -} - -export declare function transformReply(): Array; +export default { + NOT_KEYED_COMMAND: true, + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, filter: RedisVariadicArgument) { + parser.push('TS.QUERYINDEX'); + parser.pushVariadic(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..2d20b455fc1 100644 --- a/packages/time-series/lib/commands/RANGE.spec.ts +++ b/packages/time-series/lib/commands/RANGE.spec.ts @@ -1,38 +1,41 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; -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( + parseArgs(RANGE, '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..f7f808cecdb 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -1,24 +1,118 @@ -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 { 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/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 parseRangeArguments( + parser: CommandParser, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + options?: TsRangeOptions +) { + parser.push( + transformTimestampArgument(fromTimestamp), + transformTimestampArgument(toTimestamp) + ); + + if (options?.LATEST) { + parser.push('LATEST'); + } + + if (options?.FILTER_BY_TS) { + parser.push('FILTER_BY_TS'); + for (const timestamp of options.FILTER_BY_TS) { + parser.push(transformTimestampArgument(timestamp)); + } + } + + if (options?.FILTER_BY_VALUE) { + parser.push( + 'FILTER_BY_VALUE', + options.FILTER_BY_VALUE.min.toString(), + options.FILTER_BY_VALUE.max.toString() ); + } + + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); + } + + if (options?.AGGREGATION) { + if (options?.ALIGN !== undefined) { + parser.push('ALIGN', transformTimestampArgument(options.ALIGN)); + } + + parser.push( + 'AGGREGATION', + options.AGGREGATION.type, + transformTimestampArgument(options.AGGREGATION.timeBucket) + ); + + if (options.AGGREGATION.BUCKETTIMESTAMP) { + parser.push( + 'BUCKETTIMESTAMP', + options.AGGREGATION.BUCKETTIMESTAMP + ); + } + + if (options.AGGREGATION.EMPTY) { + parser.push('EMPTY'); + } + } } -export function transformReply(reply: Array): Array { - return transformRangeReply(reply); +export function transformRangeArguments( + parser: CommandParser, + key: RedisArgument, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + options?: TsRangeOptions +) { + parser.pushKey(key); + parseRangeArguments(parser, fromTimestamp, toTimestamp, options); } + +export default { + IS_READ_ONLY: true, + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('TS.RANGE'); + transformRangeArguments(...args); + }, + 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..a4c6aa2c0db 100644 --- a/packages/time-series/lib/commands/REVRANGE.spec.ts +++ b/packages/time-series/lib/commands/REVRANGE.spec.ts @@ -1,106 +1,41 @@ -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'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; + +describe('TS.REVRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(REVRANGE, '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..238b2ce9fe7 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -1,24 +1,13 @@ -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 { + IS_READ_ONLY: RANGE.IS_READ_ONLY, + 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 a29eefe860a..5b28708152f 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/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..f340861cb96 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -1,473 +1,398 @@ -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, 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'; +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 } 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, - 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 parseIgnoreArgument(parser: CommandParser, ignore?: TsIgnoreOptions) { + if (ignore !== undefined) { + parser.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 parseRetentionArgument(parser: CommandParser, retention?: number) { + if (retention !== undefined) { + parser.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 parseEncodingArgument(parser: CommandParser, encoding?: TimeSeriesEncoding) { + if (encoding !== undefined) { + parser.push('ENCODING', encoding); } } -export function pushRetentionArgument(args: RedisCommandArguments, retention?: number): RedisCommandArguments { - if (retention !== undefined) { - args.push( - 'RETENTION', - retention.toString() - ); - } - - return args; +export function parseChunkSizeArgument(parser: CommandParser, chunkSize?: number) { + if (chunkSize !== undefined) { + parser.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 parseDuplicatePolicy(parser: CommandParser, duplicatePolicy?: TimeSeriesDuplicatePolicies) { + if (duplicatePolicy !== undefined) { + parser.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 parseLabelsArgument(parser: CommandParser, labels?: Labels) { + if (labels) { + parser.push('LABELS'); - for (const [key, value] of reply) { - labels[key] = value; + for (const [label, value] of Object.entries(labels)) { + parser.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; + } } -export type SampleRawReply = [timestamp: number, value: string]; +export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>; -export interface SampleReply { - timestamp: number; - value: number; -} - -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) - ); +export type SamplesRawReply = ArrayReply; - pushLatestArgument(args, options?.LATEST); +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_TS) { - args.push('FILTER_BY_TS'); - for (const ts of options.FILTER_BY_TS) { - args.push(transformTimestampArgument(ts)); - } +// 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?.FILTER_BY_VALUE) { - args.push( - 'FILTER_BY_VALUE', - options.FILTER_BY_VALUE.min.toString(), - options.FILTER_BY_VALUE.max.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?.COUNT) { - args.push( - 'COUNT', - options.COUNT.toString() - ); + 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?.ALIGN) { - args.push( - 'ALIGN', - transformTimestampArgument(options.ALIGN) - ); + } +} + +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); } - - 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'); - } + } 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; -} - -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 { + for (const [key, value] of Object.entries(reply)) { + (reply[key] as unknown as TRANSFORMED) = 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); - } - - 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 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 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..52422bf1b5a 100644 --- a/packages/time-series/lib/index.ts +++ b/packages/time-series/lib/index.ts @@ -1,9 +1,8 @@ -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'; +export { TIME_SERIES_REDUCERS, TimeSeriesReducer } from './commands/MRANGE_GROUPBY'; diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 6d534ccccef..0f25341e34d 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -1,20 +1,21 @@ import TestUtils from '@redis/test-utils'; import TimeSeries from '.'; -export default new TestUtils({ - dockerImageName: 'redislabs/redistimeseries', - dockerImageVersionArgument: 'timeseries-version' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' }); 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..1b436701d6b 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": "5.0.1", "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": "^5.0.1" }, "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..180b3fc2ba7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,29 @@ { - "extends": "./tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": [ - "./index.ts" + "files": [], + "references": [ + { + "path": "./packages/client" + }, + { + "path": "./packages/test-utils" + }, + { + "path": "./packages/bloom" + }, + { + "path": "./packages/json" + }, + { + "path": "./packages/search" + }, + { + "path": "./packages/time-series" + }, + { + "path": "./packages/entraid" + }, + { + "path": "./packages/redis" + } ] }