From 8610a6c8369dfddd8814ca9d699ae56ec96a9426 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:15:45 +0300 Subject: [PATCH 01/19] Updated the Search package to use client@5.5.6 --- package-lock.json | 2 +- packages/search/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22e3bb8c44..6aff9edf36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7496,7 +7496,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index 6f7af2e0a2..998d785d41 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From c9ecb3d65cf54aa735674f5c4790597eb751dc9e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:16:17 +0300 Subject: [PATCH 02/19] Release search@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/search/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6aff9edf36..95d93f347b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.1.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.1.tgz", + "integrity": "sha512-bChudQmcqfYUxEGMeXMkljXtwse4hzqcqRwbZDwRyYe+EEeW/lXVl3w/mS2tHnAb2yqGnfDghid8iHEtVNqjww==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/search/package.json b/packages/search/package.json index 998d785d41..30edac8003 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From b07346e74a83c3c07c59126ddee3638ef7d7a359 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:18:25 +0300 Subject: [PATCH 03/19] Updated the Timeseries package to use client@5.5.6 --- package-lock.json | 2 +- packages/time-series/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95d93f347b..9feb22bc7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7586,7 +7586,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 4d5d8f01be..e89bab86c8 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.1.1" + "@redis/client": "^5.5.6" }, "devDependencies": { "@redis/test-utils": "*" From 7deaf336e437dfd5cea99b4e45dd54769ae7e68e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:18:52 +0300 Subject: [PATCH 04/19] Release time-series@5.5.6 --- package-lock.json | 14 +++++++++++++- packages/time-series/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9feb22bc7b..fc697256e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.1.1" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.1.1.tgz", + "integrity": "sha512-HPjZLfcZxh5mBLqRgx7KCZG6JXxGnb7yJqo9qZ/KMTWK/k3SWyH47DHJbYbRNzKOEkbK/l/5kikDTm79uJuCbg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/time-series/package.json b/packages/time-series/package.json index e89bab86c8..f90bc8f5f9 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From ba0ba71aad841a8b7511e29deeb403260ffa8e71 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:20:13 +0300 Subject: [PATCH 05/19] Updated the Redis package to use client@5.5.6 --- package-lock.json | 70 +++---------------------------------- packages/redis/package.json | 10 +++--- 2 files changed, 10 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc697256e3..af8d1a88b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7439,76 +7439,16 @@ "version": "5.1.1", "license": "MIT", "dependencies": { - "@redis/bloom": "5.1.1", - "@redis/client": "5.1.1", - "@redis/json": "5.1.1", - "@redis/search": "5.1.1", - "@redis/time-series": "5.1.1" + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.1.tgz", - "integrity": "sha512-PnMcvpL7O2DHtnSL5JtyNmraNrdHuJXi3u2isGTUuPgkbAuWQKfZdknq471ySILL+qKtLfVJqzgDFMjYmZzK6Q==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.1.tgz", - "integrity": "sha512-vojbBqUdbkD+ylCy3+ZDXLzSmgiYH9pLrv87kF+nDgsRaHKrVVxPV9B4u6EfWRx7XGvQGZqsXVkKFhsEOsG3LA==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/json": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.1.tgz", - "integrity": "sha512-A5M0dcgxGKq+oE6spIPBcGLDBiwoSPTs2wesVb4x30rXfG6rPtqt1Z7fCMtvTL2kHUNRKgZ78zhD+0+MENZt7g==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.1.tgz", - "integrity": "sha512-bChudQmcqfYUxEGMeXMkljXtwse4hzqcqRwbZDwRyYe+EEeW/lXVl3w/mS2tHnAb2yqGnfDghid8iHEtVNqjww==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.1.1.tgz", - "integrity": "sha512-HPjZLfcZxh5mBLqRgx7KCZG6JXxGnb7yJqo9qZ/KMTWK/k3SWyH47DHJbYbRNzKOEkbK/l/5kikDTm79uJuCbg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/redis/package.json b/packages/redis/package.json index 889898b3c2..bdead544fd 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.1.1", - "@redis/client": "5.1.1", - "@redis/json": "5.1.1", - "@redis/search": "5.1.1", - "@redis/time-series": "5.1.1" + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" }, "engines": { "node": ">= 18" From ca91718b594aa624571639abfe5bb8195637bc53 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 10:21:11 +0300 Subject: [PATCH 06/19] Release redis@5.5.6 --- package-lock.json | 2 +- packages/redis/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index af8d1a88b7..d9fc9f93f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,7 +7436,7 @@ } }, "packages/redis": { - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "dependencies": { "@redis/bloom": "5.5.6", diff --git a/packages/redis/package.json b/packages/redis/package.json index bdead544fd..bf5ea798e7 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.1.1", + "version": "5.5.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 62ac8b7c32473b9d0e45cbb628d05a910bc00a5f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 6 Jun 2025 15:38:52 +0300 Subject: [PATCH 07/19] fix(client): make unstable cmds throw (#2990) As per the docs, unstableResp3 commands should throw when client is created with { RESP: 3, unstableResp3: false|undefined } fixes #2989 --- docs/v5.md | 4 +-- packages/client/lib/commander.ts | 6 +++- packages/client/lib/commands/XREAD.spec.ts | 33 +++++++++++++++++++ .../client/lib/commands/XREADGROUP.spec.ts | 32 ++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/docs/v5.md b/docs/v5.md index 1784ae5bd7..15ef67c14e 100644 --- a/docs/v5.md +++ b/docs/v5.md @@ -42,9 +42,9 @@ RESP3 uses a different mechanism for handling Pub/Sub messages. Instead of modif ## Known Limitations -### Unstable Module Commands +### Unstable Commands -Some Redis module commands have unstable RESP3 transformations. These commands will throw an error when used with RESP3 unless you explicitly opt in to using them by setting `unstableResp3: true` in your client configuration: +Some Redis commands have unstable RESP3 transformations. These commands will throw an error when used with RESP3 unless you explicitly opt in to using them by setting `unstableResp3: true` in your client configuration: ```javascript const client = createClient({ diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index 6e5a2687cb..cfdf39526c 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -38,7 +38,11 @@ export function attachConfig< Class: any = class extends BaseClass {}; for (const [name, command] of Object.entries(commands)) { - Class.prototype[name] = createCommand(command, RESP); + if (config?.RESP == 3 && command.unstableResp3 && !config.unstableResp3) { + Class.prototype[name] = throwResp3SearchModuleUnstableError; + } else { + Class.prototype[name] = createCommand(command, RESP); + } } if (config?.modules) { diff --git a/packages/client/lib/commands/XREAD.spec.ts b/packages/client/lib/commands/XREAD.spec.ts index bb72c96497..0edcfe4311 100644 --- a/packages/client/lib/commands/XREAD.spec.ts +++ b/packages/client/lib/commands/XREAD.spec.ts @@ -131,4 +131,37 @@ describe('XREAD', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testWithClient('client.xRead should throw with resp3 and unstableResp3: false', async client => { + assert.throws( + () => client.xRead({ + key: 'key', + id: '0-0' + }), + { + message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance' + } + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + } + }); + + testUtils.testWithClient('client.xRead should not throw with resp3 and unstableResp3: true', async client => { + assert.doesNotThrow( + () => client.xRead({ + key: 'key', + id: '0-0' + }) + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + unstableResp3: true + } + }); + }); diff --git a/packages/client/lib/commands/XREADGROUP.spec.ts b/packages/client/lib/commands/XREADGROUP.spec.ts index 085a67bc9b..acc7cc2dea 100644 --- a/packages/client/lib/commands/XREADGROUP.spec.ts +++ b/packages/client/lib/commands/XREADGROUP.spec.ts @@ -155,4 +155,36 @@ describe('XREADGROUP', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testWithClient('client.xReadGroup should throw with resp3 and unstableResp3: false', async client => { + assert.throws( + () => client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + { + message: 'Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance' + } + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + } + }); + + testUtils.testWithClient('client.xReadGroup should not throw with resp3 and unstableResp3: true', async client => { + assert.doesNotThrow( + () => client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + unstableResp3: true + } + }); }); From 5d205cf161acce0111d344ef625e3ac4a0488b76 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 16 Jun 2025 10:20:49 +0300 Subject: [PATCH 08/19] chore(tests): bump test container version 8.0.2 (#2997) --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/sentinel/test-util.ts | 2 +- packages/client/lib/test-utils.ts | 2 +- packages/entraid/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/search/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 395e025101..d7bd649900 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: ["18", "20", "22"] - redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.1-pre"] + redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 71b423b41e..62a1f40e87 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisBloomModules from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 60c1a59689..06f8f74093 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 19bbafc66e..48736b56cd 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 11ad498f0b..1beac93913 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -6,7 +6,7 @@ import { EntraidCredentialsProvider } from './entraid-credentials-provider'; export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 9894b2d039..0d250449ac 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 7264b1b6b1..354ca16a28 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -5,7 +5,7 @@ import { RespVersions } from '@redis/client'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 0f25341e34..ab3f1cebf7 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M05-pre' + defaultDockerVersion: '8.0.2' }); export const GLOBAL = { From 2b3140bb72657af328c8b0f814c4901b0dd2ed52 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 19 Jun 2025 10:05:22 +0300 Subject: [PATCH 09/19] chore(actions): add action to close stale issues (#3000) --- .github/workflows/stale-issues.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/stale-issues.yml diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 0000000000..445af1c818 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,25 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" + +permissions: {} +jobs: + stale: + permissions: + issues: write # to close stale issues (actions/stale) + pull-requests: write # to close stale PRs (actions/stale) + + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' + stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' + days-before-stale: 365 + days-before-close: 30 + stale-issue-label: "Stale" + stale-pr-label: "Stale" + operations-per-run: 10 + remove-stale-when-updated: true From b52177752e1120f9e587b0a0062019a472f59af1 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:56:00 +0300 Subject: [PATCH 10/19] feat: added support for new bitop operations (#3001) refactored bitop tests, covered all operations updated default docker version on all packages --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/commands/BITOP.spec.ts | 70 +++++++++++++++++++++- packages/client/lib/commands/BITOP.ts | 4 +- packages/client/lib/sentinel/test-util.ts | 2 +- packages/client/lib/test-utils.ts | 2 +- packages/entraid/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/search/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 10 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d7bd649900..89efdb6111 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: ["18", "20", "22"] - redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2"] + redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2", "8.2-M01-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 62a1f40e87..4396c94f72 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisBloomModules from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/commands/BITOP.spec.ts b/packages/client/lib/commands/BITOP.spec.ts index 25fe48fc13..65fe6f8633 100644 --- a/packages/client/lib/commands/BITOP.spec.ts +++ b/packages/client/lib/commands/BITOP.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import BITOP from './BITOP'; +import BITOP, { BitOperations } from './BITOP'; import { parseArgs } from './generic-transformers'; describe('BITOP', () => { @@ -20,13 +20,77 @@ describe('BITOP', () => { }); }); - testUtils.testAll('bitOp', async client => { + for (const op of ['AND', 'OR', 'XOR'] as BitOperations[]) { + testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => { + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll(`bitOp ${op} with existing keys`, async client => { + await client.set('{tag}key1', 'value1'); + await client.set('{tag}key2', 'value2'); + + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 6 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + } + + // NOT operation requires only one key + testUtils.testAll('bitOp NOT with non-existing keys', async client => { assert.equal( - await client.bitOp('AND', '{tag}destKey', '{tag}key'), + await client.bitOp('NOT', '{tag}destKey', '{tag}key'), 0 ); }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testAll('bitOp NOT with existing keys', async client => { + await client.set('{tag}key', 'value'); + + assert.equal( + await client.bitOp('NOT', '{tag}destKey', '{tag}key'), + 5 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + // newer operations supported since Redis 8.2 + for (const op of ['DIFF', 'DIFF1', 'ANDOR', 'ONE'] as BitOperations[]) { + testUtils.testAll(`bitOp ${op} with non-existing keys`, async client => { + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 0 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + }); + + testUtils.testAll(`bitOp ${op} with existing keys`, async client => { + await client.set('{tag}key1', 'value1'); + await client.set('{tag}key2', 'value2'); + + assert.equal( + await client.bitOp(op, '{tag}destKey', ['{tag}key1', '{tag}key2']), + 6 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + }); + } }); diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts index cfce482fcb..da8b97ceda 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -2,14 +2,14 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; -export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT'; +export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT' | 'DIFF' | 'DIFF1' | 'ANDOR' | 'ONE'; export default { IS_READ_ONLY: false, /** * Performs bitwise operations between strings * @param parser - The Redis command parser - * @param operation - Bitwise operation to perform: AND, OR, XOR, NOT + * @param operation - Bitwise operation to perform: AND, OR, XOR, NOT, DIFF, DIFF1, ANDOR, ONE * @param destKey - Destination key to store the result * @param key - Source key(s) to perform operation on */ diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 06f8f74093..c8efa47f41 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 48736b56cd..809ee788e9 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 1beac93913..3c561d4ba4 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -6,7 +6,7 @@ import { EntraidCredentialsProvider } from './entraid-credentials-provider'; export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 0d250449ac..6b6859d61b 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 354ca16a28..a2b9c816da 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -5,7 +5,7 @@ import { RespVersions } from '@redis/client'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index ab3f1cebf7..8a664ee8df 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0.2' + defaultDockerVersion: '8.2-M01-pre' }); export const GLOBAL = { From c5b4f47975efea2347d923a32326808ce15a19be Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 24 Jun 2025 13:35:29 +0300 Subject: [PATCH 11/19] feat: add support for vector sets (#2998) * wip * improve the vadd api * resp3 tests * fix some tests * extract json helper functions in client package * use transformJsonReply * remove the CACHEABLE flag for all vector set commands currently, client side caching is not supported for vector set commands by the server * properly transform vinfo result * add resp3 test for vlinks * add more tests for vrandmember * fix vrem return types * fix vsetattr return type * fix vsim_withscores * implement vlinks_withscores * set minimum docker image version to 8 * align return types * add RAW variant for VEMB -> VEMB_RAW * use the new parseCommand api --- packages/client/lib/commands/VADD.spec.ts | 121 +++++++++++ packages/client/lib/commands/VADD.ts | 65 ++++++ packages/client/lib/commands/VCARD.spec.ts | 60 ++++++ packages/client/lib/commands/VCARD.ts | 18 ++ packages/client/lib/commands/VDIM.spec.ts | 43 ++++ packages/client/lib/commands/VDIM.ts | 18 ++ packages/client/lib/commands/VEMB.spec.ts | 42 ++++ packages/client/lib/commands/VEMB.ts | 21 ++ packages/client/lib/commands/VEMB_RAW.spec.ts | 68 ++++++ packages/client/lib/commands/VEMB_RAW.ts | 57 +++++ packages/client/lib/commands/VGETATTR.spec.ts | 77 +++++++ packages/client/lib/commands/VGETATTR.ts | 21 ++ packages/client/lib/commands/VINFO.spec.ts | 58 +++++ packages/client/lib/commands/VINFO.ts | 38 ++++ packages/client/lib/commands/VLINKS.spec.ts | 42 ++++ packages/client/lib/commands/VLINKS.ts | 20 ++ .../lib/commands/VLINKS_WITHSCORES.spec.ts | 75 +++++++ .../client/lib/commands/VLINKS_WITHSCORES.ts | 42 ++++ .../client/lib/commands/VRANDMEMBER.spec.ts | 201 ++++++++++++++++++ packages/client/lib/commands/VRANDMEMBER.ts | 23 ++ packages/client/lib/commands/VREM.spec.ts | 63 ++++++ packages/client/lib/commands/VREM.ts | 20 ++ packages/client/lib/commands/VSETATTR.spec.ts | 58 +++++ packages/client/lib/commands/VSETATTR.ts | 32 +++ packages/client/lib/commands/VSIM.spec.ts | 85 ++++++++ packages/client/lib/commands/VSIM.ts | 68 ++++++ .../lib/commands/VSIM_WITHSCORES.spec.ts | 62 ++++++ .../client/lib/commands/VSIM_WITHSCORES.ts | 36 ++++ .../lib/commands/generic-transformers.ts | 18 ++ packages/client/lib/commands/index.ts | 44 +++- packages/json/lib/commands/ARRAPPEND.ts | 2 +- packages/json/lib/commands/ARRINDEX.ts | 2 +- packages/json/lib/commands/ARRINSERT.ts | 2 +- packages/json/lib/commands/ARRPOP.ts | 3 +- packages/json/lib/commands/GET.ts | 3 +- packages/json/lib/commands/MERGE.ts | 2 +- packages/json/lib/commands/MGET.ts | 2 +- packages/json/lib/commands/MSET.ts | 2 +- packages/json/lib/commands/SET.ts | 2 +- packages/json/lib/commands/STRAPPEND.ts | 2 +- packages/json/lib/commands/helpers.ts | 20 -- packages/json/lib/commands/index.ts | 4 +- 42 files changed, 1608 insertions(+), 34 deletions(-) create mode 100644 packages/client/lib/commands/VADD.spec.ts create mode 100644 packages/client/lib/commands/VADD.ts create mode 100644 packages/client/lib/commands/VCARD.spec.ts create mode 100644 packages/client/lib/commands/VCARD.ts create mode 100644 packages/client/lib/commands/VDIM.spec.ts create mode 100644 packages/client/lib/commands/VDIM.ts create mode 100644 packages/client/lib/commands/VEMB.spec.ts create mode 100644 packages/client/lib/commands/VEMB.ts create mode 100644 packages/client/lib/commands/VEMB_RAW.spec.ts create mode 100644 packages/client/lib/commands/VEMB_RAW.ts create mode 100644 packages/client/lib/commands/VGETATTR.spec.ts create mode 100644 packages/client/lib/commands/VGETATTR.ts create mode 100644 packages/client/lib/commands/VINFO.spec.ts create mode 100644 packages/client/lib/commands/VINFO.ts create mode 100644 packages/client/lib/commands/VLINKS.spec.ts create mode 100644 packages/client/lib/commands/VLINKS.ts create mode 100644 packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts create mode 100644 packages/client/lib/commands/VLINKS_WITHSCORES.ts create mode 100644 packages/client/lib/commands/VRANDMEMBER.spec.ts create mode 100644 packages/client/lib/commands/VRANDMEMBER.ts create mode 100644 packages/client/lib/commands/VREM.spec.ts create mode 100644 packages/client/lib/commands/VREM.ts create mode 100644 packages/client/lib/commands/VSETATTR.spec.ts create mode 100644 packages/client/lib/commands/VSETATTR.ts create mode 100644 packages/client/lib/commands/VSIM.spec.ts create mode 100644 packages/client/lib/commands/VSIM.ts create mode 100644 packages/client/lib/commands/VSIM_WITHSCORES.spec.ts create mode 100644 packages/client/lib/commands/VSIM_WITHSCORES.ts delete mode 100644 packages/json/lib/commands/helpers.ts diff --git a/packages/client/lib/commands/VADD.spec.ts b/packages/client/lib/commands/VADD.spec.ts new file mode 100644 index 0000000000..e064beab49 --- /dev/null +++ b/packages/client/lib/commands/VADD.spec.ts @@ -0,0 +1,121 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VADD from './VADD'; +import { BasicCommandParser } from '../client/parser'; + +describe('VADD', () => { + describe('parseCommand', () => { + it('basic usage', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0, 3.0], 'element'); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '3', '1', '2', '3', 'element'] + ); + }); + + it('with REDUCE option', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2], 'element', { REDUCE: 50 }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element'] + ); + }); + + it('with quantization options', () => { + let parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'Q8' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'Q8'] + ); + + parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'BIN' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'BIN'] + ); + + parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'NOQUANT' }); + assert.deepEqual( + parser.redisArgs, + ['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'NOQUANT'] + ); + }); + + it('with all options', () => { + const parser = new BasicCommandParser(); + VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { + REDUCE: 50, + CAS: true, + QUANT: 'Q8', + EF: 200, + SETATTR: { name: 'test', value: 42 }, + M: 16 + }); + assert.deepEqual( + parser.redisArgs, + [ + 'VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element', + 'CAS', 'Q8', 'EF', '200', 'SETATTR', '{"name":"test","value":42}', 'M', '16' + ] + ); + }); + }); + + testUtils.testAll('vAdd', async client => { + assert.equal( + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'), + true + ); + + // same element should not be added again + assert.equal( + await client.vAdd('key', [1, 2 , 3], 'element'), + false + ); + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }, + }); + + testUtils.testWithClient('vAdd with RESP3', async client => { + // Test basic functionality with RESP3 + assert.equal( + await client.vAdd('resp3-key', [1.5, 2.5, 3.5], 'resp3-element'), + true + ); + + // same element should not be added again + assert.equal( + await client.vAdd('resp3-key', [1, 2 , 3], 'resp3-element'), + false + ); + + // Test with options to ensure complex parameters work with RESP3 + assert.equal( + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'resp3-element2', { + QUANT: 'Q8', + CAS: true, + SETATTR: { type: 'test', value: 123 } + }), + true + ); + + // Verify the vector set was created correctly + assert.equal( + await client.vCard('resp3-key'), + 2 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VADD.ts b/packages/client/lib/commands/VADD.ts new file mode 100644 index 0000000000..0406bd58d0 --- /dev/null +++ b/packages/client/lib/commands/VADD.ts @@ -0,0 +1,65 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply, transformDoubleArgument } from './generic-transformers'; + +export interface VAddOptions { + REDUCE?: number; + CAS?: boolean; + QUANT?: 'NOQUANT' | 'BIN' | 'Q8', + EF?: number; + SETATTR?: Record; + M?: number; +} + +export default { + /** + * Add a new element into the vector set specified by key + * + * @param parser - The command parser + * @param key - The name of the key that will hold the vector set data + * @param vector - The vector data as array of numbers + * @param element - The name of the element being added to the vector set + * @param options - Optional parameters for vector addition + * @see https://redis.io/commands/vadd/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + vector: Array, + element: RedisArgument, + options?: VAddOptions + ) { + parser.push('VADD'); + parser.pushKey(key); + + if (options?.REDUCE !== undefined) { + parser.push('REDUCE', options.REDUCE.toString()); + } + + parser.push('VALUES', vector.length.toString()); + for (const value of vector) { + parser.push(transformDoubleArgument(value)); + } + + parser.push(element); + + if (options?.CAS) { + parser.push('CAS'); + } + + options?.QUANT && parser.push(options.QUANT); + + if (options?.EF !== undefined) { + parser.push('EF', options.EF.toString()); + } + + if (options?.SETATTR) { + parser.push('SETATTR', JSON.stringify(options.SETATTR)); + } + + if (options?.M !== undefined) { + parser.push('M', options.M.toString()); + } + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VCARD.spec.ts b/packages/client/lib/commands/VCARD.spec.ts new file mode 100644 index 0000000000..feb9040fcb --- /dev/null +++ b/packages/client/lib/commands/VCARD.spec.ts @@ -0,0 +1,60 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VCARD from './VCARD'; +import { BasicCommandParser } from '../client/parser'; + +describe('VCARD', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VCARD.parseCommand(parser, 'key') + assert.deepEqual( + parser.redisArgs, + ['VCARD', 'key'] + ); + }); + + testUtils.testAll('vCard', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + assert.equal( + await client.vCard('key'), + 2 + ); + + assert.equal(await client.vCard('unknown'), 0); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vCard with RESP3', async client => { + // Test empty vector set + assert.equal( + await client.vCard('resp3-empty-key'), + 0 + ); + + // Add elements and test cardinality + await client.vAdd('resp3-key', [1.0, 2.0], 'elem1'); + assert.equal( + await client.vCard('resp3-key'), + 1 + ); + + await client.vAdd('resp3-key', [3.0, 4.0], 'elem2'); + await client.vAdd('resp3-key', [5.0, 6.0], 'elem3'); + assert.equal( + await client.vCard('resp3-key'), + 3 + ); + + assert.equal(await client.vCard('unknown'), 0); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VCARD.ts b/packages/client/lib/commands/VCARD.ts new file mode 100644 index 0000000000..575abf9b71 --- /dev/null +++ b/packages/client/lib/commands/VCARD.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the number of elements in a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vcard/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VCARD'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VDIM.spec.ts b/packages/client/lib/commands/VDIM.spec.ts new file mode 100644 index 0000000000..db3f5f3bd8 --- /dev/null +++ b/packages/client/lib/commands/VDIM.spec.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VDIM from './VDIM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VDIM', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VDIM.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VDIM', 'key'] + ); + }); + + testUtils.testAll('vDim', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vDim('key'), + 3 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vDim with RESP3', async client => { + await client.vAdd('resp3-5d', [1.0, 2.0, 3.0, 4.0, 5.0], 'elem5d'); + + assert.equal( + await client.vDim('resp3-5d'), + 5 + ); + + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VDIM.ts b/packages/client/lib/commands/VDIM.ts new file mode 100644 index 0000000000..f7933e77ea --- /dev/null +++ b/packages/client/lib/commands/VDIM.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the dimension of the vectors in a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vdim/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VDIM'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VEMB.spec.ts b/packages/client/lib/commands/VEMB.spec.ts new file mode 100644 index 0000000000..ed9515ebdd --- /dev/null +++ b/packages/client/lib/commands/VEMB.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VEMB from './VEMB'; +import { BasicCommandParser } from '../client/parser'; + +describe('VEMB', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VEMB.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VEMB', 'key', 'element'] + ); + }); + + testUtils.testAll('vEmb', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const result = await client.vEmb('key', 'element'); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 3); + assert.equal(typeof result[0], 'number'); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vEmb with RESP3', async client => { + await client.vAdd('resp3-key', [1.5, 2.5, 3.5, 4.5], 'resp3-element'); + + const result = await client.vEmb('resp3-key', 'resp3-element'); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 4); + assert.equal(typeof result[0], 'number'); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VEMB.ts b/packages/client/lib/commands/VEMB.ts new file mode 100644 index 0000000000..d534c27d65 --- /dev/null +++ b/packages/client/lib/commands/VEMB.ts @@ -0,0 +1,21 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformDoubleArrayReply } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the approximate vector associated with a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve the vector for + * @see https://redis.io/commands/vemb/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VEMB'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformDoubleArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VEMB_RAW.spec.ts b/packages/client/lib/commands/VEMB_RAW.spec.ts new file mode 100644 index 0000000000..33d3af8540 --- /dev/null +++ b/packages/client/lib/commands/VEMB_RAW.spec.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VEMB_RAW from './VEMB_RAW'; +import { BasicCommandParser } from '../client/parser'; + +describe('VEMB_RAW', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VEMB_RAW.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VEMB', 'key', 'element', 'RAW'] + ); + }); + + testUtils.testAll('vEmbRaw', async client => { + await client.vAdd('key1', [1.0, 2.0, 3.0], 'element'); + const result1 = await client.vEmbRaw('key1', 'element'); + assert.equal(result1.quantization, 'int8'); + assert.ok(result1.quantizationRange !== undefined); + + await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' }); + const result2 = await client.vEmbRaw('key2', 'element'); + assert.equal(result2.quantization, 'int8'); + assert.ok(result2.quantizationRange !== undefined); + + await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' }); + const result3 = await client.vEmbRaw('key3', 'element'); + assert.equal(result3.quantization, 'f32'); + assert.equal(result3.quantizationRange, undefined); + + await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' }); + const result4 = await client.vEmbRaw('key4', 'element'); + assert.equal(result4.quantization, 'bin'); + assert.equal(result4.quantizationRange, undefined); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vEmbRaw with RESP3', async client => { + await client.vAdd('key1', [1.0, 2.0, 3.0], 'element'); + const result1 = await client.vEmbRaw('key1', 'element'); + assert.equal(result1.quantization, 'int8'); + assert.ok(result1.quantizationRange !== undefined); + + await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' }); + const result2 = await client.vEmbRaw('key2', 'element'); + assert.equal(result2.quantization, 'int8'); + assert.ok(result2.quantizationRange !== undefined); + + await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' }); + const result3 = await client.vEmbRaw('key3', 'element'); + assert.equal(result3.quantization, 'f32'); + assert.equal(result3.quantizationRange, undefined); + + await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' }); + const result4 = await client.vEmbRaw('key4', 'element'); + assert.equal(result4.quantization, 'bin'); + assert.equal(result4.quantizationRange, undefined); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VEMB_RAW.ts b/packages/client/lib/commands/VEMB_RAW.ts new file mode 100644 index 0000000000..b6881d321c --- /dev/null +++ b/packages/client/lib/commands/VEMB_RAW.ts @@ -0,0 +1,57 @@ +import { CommandParser } from '../client/parser'; +import { + RedisArgument, + Command, + BlobStringReply, + SimpleStringReply, + DoubleReply +} from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VEMB from './VEMB'; + +type RawVembReply = { + quantization: SimpleStringReply; + raw: BlobStringReply; + l2Norm: DoubleReply; + quantizationRange?: DoubleReply; +}; + +const transformRawVembReply = { + 2: (reply: any[]): RawVembReply => { + return { + quantization: reply[0], + raw: reply[1], + l2Norm: transformDoubleReply[2](reply[2]), + ...(reply[3] !== undefined && { quantizationRange: transformDoubleReply[2](reply[3]) }) + }; + }, + 3: (reply: any[]): RawVembReply => { + return { + quantization: reply[0], + raw: reply[1], + l2Norm: reply[2], + quantizationRange: reply[3] + }; + }, +}; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the RAW approximate vector associated with a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve the vector for + * @see https://redis.io/commands/vemb/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + element: RedisArgument + ) { + VEMB.parseCommand(parser, key, element); + parser.push('RAW'); + }, + transformReply: transformRawVembReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VGETATTR.spec.ts b/packages/client/lib/commands/VGETATTR.spec.ts new file mode 100644 index 0000000000..d904146c67 --- /dev/null +++ b/packages/client/lib/commands/VGETATTR.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VGETATTR from './VGETATTR'; +import { BasicCommandParser } from '../client/parser'; + +describe('VGETATTR', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VGETATTR.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VGETATTR', 'key', 'element'] + ); + }); + + testUtils.testAll('vGetAttr', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const nullResult = await client.vGetAttr('key', 'element'); + assert.equal(nullResult, null); + + await client.vSetAttr('key', 'element', { name: 'test' }); + + const result = await client.vGetAttr('key', 'element'); + + assert.ok(result !== null); + assert.equal(typeof result, 'object') + + assert.deepEqual(result, { + name: 'test' + }) + + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vGetAttr with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0], 'resp3-element'); + + // Test null case (no attributes set) + const nullResult = await client.vGetAttr('resp3-key', 'resp3-element'); + + assert.equal(nullResult, null); + + // Set complex attributes and retrieve them + const complexAttrs = { + name: 'test-item', + category: 'electronics', + price: 99.99, + inStock: true, + tags: ['new', 'featured'] + }; + await client.vSetAttr('resp3-key', 'resp3-element', complexAttrs); + + const result = await client.vGetAttr('resp3-key', 'resp3-element'); + + assert.ok(result !== null); + assert.equal(typeof result, 'object') + + assert.deepEqual(result, { + name: 'test-item', + category: 'electronics', + price: 99.99, + inStock: true, + tags: ['new', 'featured'] + }) + + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VGETATTR.ts b/packages/client/lib/commands/VGETATTR.ts new file mode 100644 index 0000000000..05ec8706fb --- /dev/null +++ b/packages/client/lib/commands/VGETATTR.ts @@ -0,0 +1,21 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformRedisJsonNullReply } from './generic-transformers'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the attributes of a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve attributes for + * @see https://redis.io/commands/vgetattr/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VGETATTR'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformRedisJsonNullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VINFO.spec.ts b/packages/client/lib/commands/VINFO.spec.ts new file mode 100644 index 0000000000..074598644f --- /dev/null +++ b/packages/client/lib/commands/VINFO.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VINFO from './VINFO'; +import { BasicCommandParser } from '../client/parser'; + +describe('VINFO', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VINFO.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VINFO', 'key'] + ); + }); + + testUtils.testAll('vInfo', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + const result = await client.vInfo('key'); + assert.ok(typeof result === 'object' && result !== null); + + assert.equal(result['vector-dim'], 3); + assert.equal(result['size'], 1); + assert.ok('quant-type' in result); + assert.ok('hnsw-m' in result); + assert.ok('projection-input-dim' in result); + assert.ok('max-level' in result); + assert.ok('attributes-count' in result); + assert.ok('vset-uid' in result); + assert.ok('hnsw-max-node-uid' in result); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vInfo with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + const result = await client.vInfo('resp3-key'); + assert.ok(typeof result === 'object' && result !== null); + + assert.equal(result['vector-dim'], 3); + assert.equal(result['size'], 1); + assert.ok('quant-type' in result); + assert.ok('hnsw-m' in result); + assert.ok('projection-input-dim' in result); + assert.ok('max-level' in result); + assert.ok('attributes-count' in result); + assert.ok('vset-uid' in result); + assert.ok('hnsw-max-node-uid' in result); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VINFO.ts b/packages/client/lib/commands/VINFO.ts new file mode 100644 index 0000000000..4e0d68d7cb --- /dev/null +++ b/packages/client/lib/commands/VINFO.ts @@ -0,0 +1,38 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command, UnwrapReply, Resp2Reply, TuplesToMapReply, SimpleStringReply, NumberReply } from '../RESP/types'; + +export type VInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'quant-type'>, SimpleStringReply], + [SimpleStringReply<'vector-dim'>, NumberReply], + [SimpleStringReply<'size'>, NumberReply], + [SimpleStringReply<'max-level'>, NumberReply], + [SimpleStringReply<'vset-uid'>, NumberReply], + [SimpleStringReply<'hnsw-max-node-uid'>, NumberReply], +]>; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve metadata and internal details about a vector set, including size, dimensions, quantization type, and graph structure + * + * @param parser - The command parser + * @param key - The key of the vector set + * @see https://redis.io/commands/vinfo/ + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('VINFO'); + parser.pushKey(key); + }, + transformReply: { + 2: (reply: UnwrapReply>): VInfoReplyMap => { + const ret = Object.create(null); + + for (let i = 0; i < reply.length; i += 2) { + ret[reply[i].toString()] = reply[i + 1]; + } + + return ret as unknown as VInfoReplyMap; + }, + 3: undefined as unknown as () => VInfoReplyMap + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/VLINKS.spec.ts b/packages/client/lib/commands/VLINKS.spec.ts new file mode 100644 index 0000000000..e788f9f9a9 --- /dev/null +++ b/packages/client/lib/commands/VLINKS.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VLINKS from './VLINKS'; +import { BasicCommandParser } from '../client/parser'; + +describe('VLINKS', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VLINKS.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VLINKS', 'key', 'element'] + ); + }); + + testUtils.testAll('vLinks', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vLinks('key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.length) + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vLinks with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vLinks('resp3-key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.length) + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VLINKS.ts b/packages/client/lib/commands/VLINKS.ts new file mode 100644 index 0000000000..9e97fc7de9 --- /dev/null +++ b/packages/client/lib/commands/VLINKS.ts @@ -0,0 +1,20 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve the neighbors of a specified element in a vector set; the connections for each layer of the HNSW graph + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to retrieve neighbors for + * @see https://redis.io/commands/vlinks/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VLINKS'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: undefined as unknown as () => ArrayReply> +} as const satisfies Command; diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts new file mode 100644 index 0000000000..db96bd1a8a --- /dev/null +++ b/packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts @@ -0,0 +1,75 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VLINKS_WITHSCORES from './VLINKS_WITHSCORES'; +import { BasicCommandParser } from '../client/parser'; + +describe('VLINKS WITHSCORES', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VLINKS_WITHSCORES.parseCommand(parser, 'key', 'element'); + assert.deepEqual(parser.redisArgs, [ + 'VLINKS', + 'key', + 'element', + 'WITHSCORES' + ]); + }); + + testUtils.testAll( + 'vLinksWithScores', + async client => { + // Create a vector set with multiple elements to build HNSW graph layers + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('key', [1.2, 2.2, 3.2], 'element3'); + await client.vAdd('key', [2.0, 3.0, 4.0], 'element4'); + + const result = await client.vLinksWithScores('key', 'element1'); + + assert.ok(Array.isArray(result)); + + for (const layer of result) { + assert.equal( + typeof layer, + 'object' + ); + } + + assert.ok(result.length >= 1, 'Should have at least layer 0'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + } + ); + + testUtils.testWithClient( + 'vLinksWithScores with RESP3', + async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [1.2, 2.2, 3.2], 'element3'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element4'); + + const result = await client.vLinksWithScores('resp3-key', 'element1'); + + assert.ok(Array.isArray(result)); + + for (const layer of result) { + assert.equal( + typeof layer, + 'object' + ); + } + + assert.ok(result.length >= 1, 'Should have at least layer 0'); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + } + ); +}); diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.ts new file mode 100644 index 0000000000..10ebe160fc --- /dev/null +++ b/packages/client/lib/commands/VLINKS_WITHSCORES.ts @@ -0,0 +1,42 @@ +import { BlobStringReply, Command, DoubleReply, MapReply } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VLINKS from './VLINKS'; + + +function transformVLinksWithScoresReply(reply: any): Array> { + const layers: Array> = []; + + for (const layer of reply) { + const obj: Record = Object.create(null); + + // Each layer contains alternating element names and scores + for (let i = 0; i < layer.length; i += 2) { + const element = layer[i]; + const score = transformDoubleReply[2](layer[i + 1]); + obj[element.toString()] = score; + } + + layers.push(obj); + } + + return layers; +} + +export default { + IS_READ_ONLY: VLINKS.IS_READ_ONLY, + /** + * Get the connections for each layer of the HNSW graph with similarity scores + * @param args - Same parameters as the VLINKS command + * @see https://redis.io/commands/vlinks/ + */ + parseCommand(...args: Parameters) { + const parser = args[0]; + + VLINKS.parseCommand(...args); + parser.push('WITHSCORES'); + }, + transformReply: { + 2: transformVLinksWithScoresReply, + 3: undefined as unknown as () => Array> + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/VRANDMEMBER.spec.ts b/packages/client/lib/commands/VRANDMEMBER.spec.ts new file mode 100644 index 0000000000..28c020e356 --- /dev/null +++ b/packages/client/lib/commands/VRANDMEMBER.spec.ts @@ -0,0 +1,201 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VRANDMEMBER from './VRANDMEMBER'; +import { BasicCommandParser } from '../client/parser'; + +describe('VRANDMEMBER', () => { + describe('parseCommand', () => { + it('without count', () => { + const parser = new BasicCommandParser(); + VRANDMEMBER.parseCommand(parser, 'key'); + assert.deepEqual( + parser.redisArgs, + ['VRANDMEMBER', 'key'] + ); + }); + + it('with count', () => { + const parser = new BasicCommandParser(); + VRANDMEMBER.parseCommand(parser, 'key', 2); + assert.deepEqual( + parser.redisArgs, + ['VRANDMEMBER', 'key', '2'] + ); + }); + }); + + describe('RESP2 tests', () => { + testUtils.testAll('vRandMember without count - returns single element as string', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('key'); + assert.equal(typeof result, 'string'); + assert.ok(['element1', 'element2', 'element3'].includes(result as string)); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember with positive count - returns distinct elements', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('key', 2); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); + + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember with negative count - allows duplicates', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('key', -5); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 5); + + // All elements should be from our set (duplicates allowed) + result.forEach(element => { + assert.ok(['element1', 'element2'].includes(element)); + }); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember count exceeds set size - returns entire set', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('key', 10); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); // Only 2 elements exist + + // Should contain both elements + assert.ok(result.includes('element1')); + assert.ok(result.includes('element2')); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testAll('vRandMember on non-existent key', async client => { + // Without count - should return null + const resultNoCount = await client.vRandMember('nonexistent'); + assert.equal(resultNoCount, null); + + // With count - should return empty array + const resultWithCount = await client.vRandMember('nonexistent', 5); + assert.ok(Array.isArray(resultWithCount)); + assert.equal(resultWithCount.length, 0); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + }); + + describe('RESP3 tests', () => { + testUtils.testWithClient('vRandMember without count - returns single element as string', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('resp3-key'); + assert.equal(typeof result, 'string'); + assert.ok(['element1', 'element2', 'element3'].includes(result as string)); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember with positive count - returns distinct elements', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3'); + + const result = await client.vRandMember('resp3-key', 2); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); + + // Should be distinct elements (no duplicates) + const uniqueElements = new Set(result); + assert.equal(uniqueElements.size, 2); + + // All elements should be from our set + result.forEach(element => { + assert.ok(['element1', 'element2', 'element3'].includes(element)); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember with negative count - allows duplicates', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('resp3-key', -5); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 5); + + // All elements should be from our set (duplicates allowed) + result.forEach(element => { + assert.ok(['element1', 'element2'].includes(element)); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember count exceeds set size - returns entire set', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2'); + + const result = await client.vRandMember('resp3-key', 10); + assert.ok(Array.isArray(result)); + assert.equal(result.length, 2); // Only 2 elements exist + + // Should contain both elements + assert.ok(result.includes('element1')); + assert.ok(result.includes('element2')); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + + testUtils.testWithClient('vRandMember on non-existent key', async client => { + // Without count - should return null + const resultNoCount = await client.vRandMember('resp3-nonexistent'); + assert.equal(resultNoCount, null); + + // With count - should return empty array + const resultWithCount = await client.vRandMember('resp3-nonexistent', 5); + assert.ok(Array.isArray(resultWithCount)); + assert.equal(resultWithCount.length, 0); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); + }); +}); diff --git a/packages/client/lib/commands/VRANDMEMBER.ts b/packages/client/lib/commands/VRANDMEMBER.ts new file mode 100644 index 0000000000..299af33b9f --- /dev/null +++ b/packages/client/lib/commands/VRANDMEMBER.ts @@ -0,0 +1,23 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, ArrayReply, Command, NullReply } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + /** + * Retrieve random elements of a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param count - Optional number of elements to return + * @see https://redis.io/commands/vrandmember/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, count?: number) { + parser.push('VRANDMEMBER'); + parser.pushKey(key); + + if (count !== undefined) { + parser.push(count.toString()); + } + }, + transformReply: undefined as unknown as () => BlobStringReply | ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VREM.spec.ts b/packages/client/lib/commands/VREM.spec.ts new file mode 100644 index 0000000000..9e558c991c --- /dev/null +++ b/packages/client/lib/commands/VREM.spec.ts @@ -0,0 +1,63 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VREM from './VREM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VREM', () => { + const parser = new BasicCommandParser(); + VREM.parseCommand(parser, 'key', 'element'); + it('parseCommand', () => { + assert.deepEqual( + parser.redisArgs, + ['VREM', 'key', 'element'] + ); + }); + + testUtils.testAll('vRem', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vRem('key', 'element'), + true + ); + + assert.equal( + await client.vRem('key', 'element'), + false + ); + + assert.equal( + await client.vCard('key'), + 0 + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vRem with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + assert.equal( + await client.vRem('resp3-key', 'resp3-element'), + true + ); + + assert.equal( + await client.vRem('resp3-key', 'resp3-element'), + false + ); + + + assert.equal( + await client.vCard('resp3-key'), + 0 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VREM.ts b/packages/client/lib/commands/VREM.ts new file mode 100644 index 0000000000..7eb22b2e2e --- /dev/null +++ b/packages/client/lib/commands/VREM.ts @@ -0,0 +1,20 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply } from './generic-transformers'; + +export default { + /** + * Remove an element from a vector set + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to remove from the vector set + * @see https://redis.io/commands/vrem/ + */ + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { + parser.push('VREM'); + parser.pushKey(key); + parser.push(element); + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSETATTR.spec.ts b/packages/client/lib/commands/VSETATTR.spec.ts new file mode 100644 index 0000000000..303006d408 --- /dev/null +++ b/packages/client/lib/commands/VSETATTR.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSETATTR from './VSETATTR'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSETATTR', () => { + describe('parseCommand', () => { + it('with object', () => { + const parser = new BasicCommandParser(); + VSETATTR.parseCommand(parser, 'key', 'element', { name: 'test', value: 42 }), + assert.deepEqual( + parser.redisArgs, + ['VSETATTR', 'key', 'element', '{"name":"test","value":42}'] + ); + }); + + it('with string', () => { + const parser = new BasicCommandParser(); + VSETATTR.parseCommand(parser, 'key', 'element', '{"name":"test"}'), + assert.deepEqual( + parser.redisArgs, + ['VSETATTR', 'key', 'element', '{"name":"test"}'] + ); + }); + }); + + testUtils.testAll('vSetAttr', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element'); + + assert.equal( + await client.vSetAttr('key', 'element', { name: 'test', value: 42 }), + true + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vSetAttr with RESP3 - returns boolean', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element'); + + const result = await client.vSetAttr('resp3-key', 'resp3-element', { + name: 'test-item', + category: 'electronics', + price: 99.99 + }); + + // RESP3 returns boolean instead of number + assert.equal(typeof result, 'boolean'); + assert.equal(result, true); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VSETATTR.ts b/packages/client/lib/commands/VSETATTR.ts new file mode 100644 index 0000000000..084b8f8008 --- /dev/null +++ b/packages/client/lib/commands/VSETATTR.ts @@ -0,0 +1,32 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformBooleanReply } from './generic-transformers'; + +export default { + /** + * Set or replace attributes on a vector set element + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param element - The name of the element to set attributes for + * @param attributes - The attributes to set (as JSON string or object) + * @see https://redis.io/commands/vsetattr/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + element: RedisArgument, + attributes: RedisArgument | Record + ) { + parser.push('VSETATTR'); + parser.pushKey(key); + parser.push(element); + + if (typeof attributes === 'object' && attributes !== null) { + parser.push(JSON.stringify(attributes)); + } else { + parser.push(attributes); + } + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSIM.spec.ts b/packages/client/lib/commands/VSIM.spec.ts new file mode 100644 index 0000000000..b7e10eb6c4 --- /dev/null +++ b/packages/client/lib/commands/VSIM.spec.ts @@ -0,0 +1,85 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSIM from './VSIM'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSIM', () => { + describe('parseCommand', () => { + it('with vector', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', [1.0, 2.0, 3.0]), + assert.deepEqual( + parser.redisArgs, + ['VSIM', 'key', 'VALUES', '3', '1', '2', '3'] + ); + }); + + it('with element', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', 'element'); + assert.deepEqual( + parser.redisArgs, + ['VSIM', 'key', 'ELE', 'element'] + ); + }); + + it('with options', () => { + const parser = new BasicCommandParser(); + VSIM.parseCommand(parser, 'key', 'element', { + COUNT: 5, + EF: 100, + FILTER: '.price > 20', + 'FILTER-EF': 50, + TRUTH: true, + NOTHREAD: true + }); + assert.deepEqual( + parser.redisArgs, + [ + 'VSIM', 'key', 'ELE', 'element', + 'COUNT', '5', 'EF', '100', 'FILTER', '.price > 20', + 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD' + ] + ); + }); + }); + + testUtils.testAll('vSim', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSim('key', 'element1'); + assert.ok(Array.isArray(result)); + assert.ok(result.includes('element1')); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + + testUtils.testWithClient('vSim with RESP3', async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3'); + + // Test similarity search with vector + const resultWithVector = await client.vSim('resp3-key', [1.05, 2.05, 3.05]); + assert.ok(Array.isArray(resultWithVector)); + assert.ok(resultWithVector.length > 0); + + // Test similarity search with element + const resultWithElement = await client.vSim('resp3-key', 'element1'); + assert.ok(Array.isArray(resultWithElement)); + assert.ok(resultWithElement.includes('element1')); + + // Test with options + const resultWithOptions = await client.vSim('resp3-key', 'element1', { COUNT: 2 }); + assert.ok(Array.isArray(resultWithOptions)); + assert.ok(resultWithOptions.length <= 2); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + }); +}); diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts new file mode 100644 index 0000000000..dc41a54caf --- /dev/null +++ b/packages/client/lib/commands/VSIM.ts @@ -0,0 +1,68 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformDoubleArgument } from './generic-transformers'; + +export interface VSimOptions { + COUNT?: number; + EF?: number; + FILTER?: string; + 'FILTER-EF'?: number; + TRUTH?: boolean; + NOTHREAD?: boolean; +} + +export default { + IS_READ_ONLY: true, + /** + * Retrieve elements similar to a given vector or element with optional filtering + * + * @param parser - The command parser + * @param key - The key of the vector set + * @param query - The query vector (array of numbers) or element name (string) + * @param options - Optional parameters for similarity search + * @see https://redis.io/commands/vsim/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + query: RedisArgument | Array, + options?: VSimOptions + ) { + parser.push('VSIM'); + parser.pushKey(key); + + if (Array.isArray(query)) { + parser.push('VALUES', query.length.toString()); + for (const value of query) { + parser.push(transformDoubleArgument(value)); + } + } else { + parser.push('ELE', query); + } + + if (options?.COUNT !== undefined) { + parser.push('COUNT', options.COUNT.toString()); + } + + if (options?.EF !== undefined) { + parser.push('EF', options.EF.toString()); + } + + if (options?.FILTER) { + parser.push('FILTER', options.FILTER); + } + + if (options?.['FILTER-EF'] !== undefined) { + parser.push('FILTER-EF', options['FILTER-EF'].toString()); + } + + if (options?.TRUTH) { + parser.push('TRUTH'); + } + + if (options?.NOTHREAD) { + parser.push('NOTHREAD'); + } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts new file mode 100644 index 0000000000..ff9bc41376 --- /dev/null +++ b/packages/client/lib/commands/VSIM_WITHSCORES.spec.ts @@ -0,0 +1,62 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import VSIM_WITHSCORES from './VSIM_WITHSCORES'; +import { BasicCommandParser } from '../client/parser'; + +describe('VSIM WITHSCORES', () => { + it('parseCommand', () => { + const parser = new BasicCommandParser(); + VSIM_WITHSCORES.parseCommand(parser, 'key', 'element') + assert.deepEqual(parser.redisArgs, [ + 'VSIM', + 'key', + 'ELE', + 'element', + 'WITHSCORES' + ]); + }); + + testUtils.testAll( + 'vSimWithScores', + async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSimWithScores('key', 'element1'); + + assert.ok(typeof result === 'object'); + assert.ok('element1' in result); + assert.ok('element2' in result); + assert.equal(typeof result['element1'], 'number'); + assert.equal(typeof result['element2'], 'number'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + } + ); + + testUtils.testWithClient( + 'vSimWithScores with RESP3 - returns Map with scores', + async client => { + await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); + await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3'); + + const result = await client.vSimWithScores('resp3-key', 'element1'); + + assert.ok(typeof result === 'object'); + assert.ok('element1' in result); + assert.ok('element2' in result); + assert.equal(typeof result['element1'], 'number'); + assert.equal(typeof result['element2'], 'number'); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3 + }, + minimumDockerVersion: [8, 0] + } + ); +}); diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.ts b/packages/client/lib/commands/VSIM_WITHSCORES.ts new file mode 100644 index 0000000000..fda05be664 --- /dev/null +++ b/packages/client/lib/commands/VSIM_WITHSCORES.ts @@ -0,0 +1,36 @@ +import { + ArrayReply, + BlobStringReply, + Command, + DoubleReply, + MapReply, + UnwrapReply +} from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; +import VSIM from './VSIM'; + +export default { + IS_READ_ONLY: VSIM.IS_READ_ONLY, + /** + * Retrieve elements similar to a given vector or element with similarity scores + * @param args - Same parameters as the VSIM command + * @see https://redis.io/commands/vsim/ + */ + parseCommand(...args: Parameters) { + const parser = args[0]; + + VSIM.parseCommand(...args); + parser.push('WITHSCORES'); + }, + transformReply: { + 2: (reply: ArrayReply) => { + const inferred = reply as unknown as UnwrapReply; + const members: Record = {}; + for (let i = 0; i < inferred.length; i += 2) { + members[inferred[i].toString()] = transformDoubleReply[2](inferred[i + 1]); + } + return members; + }, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index 91eab7107a..022339e4bb 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -662,3 +662,21 @@ export function transformStreamsMessagesReplyResp3(reply: UnwrapReply } } + +export type RedisJSON = null | boolean | number | string | Date | Array | { + [key: string]: RedisJSON; + [key: number]: RedisJSON; +}; + +export function transformRedisJsonArgument(json: RedisJSON): string { + return JSON.stringify(json); +} + +export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { + const res = JSON.parse((json as unknown as UnwrapReply).toString()); + return res; +} + +export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { + return isNullReply(json) ? json : transformRedisJsonReply(json); +} diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 5cd81331a4..87ab8d10b8 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -344,6 +344,20 @@ import ZSCORE from './ZSCORE'; import ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; import ZUNION from './ZUNION'; import ZUNIONSTORE from './ZUNIONSTORE'; +import VADD from './VADD'; +import VCARD from './VCARD'; +import VDIM from './VDIM'; +import VEMB from './VEMB'; +import VEMB_RAW from './VEMB_RAW'; +import VGETATTR from './VGETATTR'; +import VINFO from './VINFO'; +import VLINKS from './VLINKS'; +import VLINKS_WITHSCORES from './VLINKS_WITHSCORES'; +import VRANDMEMBER from './VRANDMEMBER'; +import VREM from './VREM'; +import VSETATTR from './VSETATTR'; +import VSIM from './VSIM'; +import VSIM_WITHSCORES from './VSIM_WITHSCORES'; export default { ACL_CAT, @@ -1037,5 +1051,33 @@ export default { ZUNION, zUnion: ZUNION, ZUNIONSTORE, - zUnionStore: ZUNIONSTORE + zUnionStore: ZUNIONSTORE, + VADD, + vAdd: VADD, + VCARD, + vCard: VCARD, + VDIM, + vDim: VDIM, + VEMB, + vEmb: VEMB, + VEMB_RAW, + vEmbRaw: VEMB_RAW, + VGETATTR, + vGetAttr: VGETATTR, + VINFO, + vInfo: VINFO, + VLINKS, + vLinks: VLINKS, + VLINKS_WITHSCORES, + vLinksWithScores: VLINKS_WITHSCORES, + VRANDMEMBER, + vRandMember: VRANDMEMBER, + VREM, + vRem: VREM, + VSETATTR, + vSetAttr: VSETATTR, + VSIM, + vSim: VSIM, + VSIM_WITHSCORES, + vSimWithScores: VSIM_WITHSCORES } as const satisfies RedisCommands; diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index d1082baf48..b98d1532b4 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,5 +1,5 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 69485f55a6..1437fab4d5 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonArrIndexOptions { range?: { diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index 33fe30a99e..7a5ab94589 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 53d9ed2dc8..88e4da9698 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,7 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -import { isArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -import { transformRedisJsonNullReply } from './helpers'; +import { isArrayReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface RedisArrPopOptions { path: RedisArgument; diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index e514fefae3..14ec46a53a 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,7 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { transformRedisJsonNullReply } from './helpers'; +import { RedisVariadicArgument, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonGetOptions { path?: RedisVariadicArgument; diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 72baea1048..1a4b54fc4b 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index 7bb948bc66..01a7783b92 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformRedisJsonNullReply } from './helpers'; +import { transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index 9e5ec1799f..81e8d4c6bd 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonMSetItem { key: RedisArgument; diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index a0df41fa89..9ab680b489 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisJSON, transformRedisJsonArgument } from './helpers'; +import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonSetOptions { condition?: 'NX' | 'XX'; diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index aa8f3772fb..b3115f684c 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,6 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types'; -import { transformRedisJsonArgument } from './helpers'; +import { transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface JsonStrAppendOptions { path?: RedisArgument; diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts deleted file mode 100644 index 99579ce81c..0000000000 --- a/packages/json/lib/commands/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformers"; -import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; - -export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - return isNullReply(json) ? json : transformRedisJsonReply(json); -} - -export type RedisJSON = null | boolean | number | string | Date | Array | { - [key: string]: RedisJSON; - [key: number]: RedisJSON; -}; - -export function transformRedisJsonArgument(json: RedisJSON): string { - return JSON.stringify(json); -} - -export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { - const res = JSON.parse((json as unknown as UnwrapReply).toString()); - return res; -} diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index a9e16bde75..0e29bdd648 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -23,7 +23,9 @@ import STRLEN from './STRLEN'; import TOGGLE from './TOGGLE'; import TYPE from './TYPE'; -export * from './helpers'; +// Re-export helper types and functions from client package +export type { RedisJSON } from '@redis/client/dist/lib/commands/generic-transformers'; +export { transformRedisJsonArgument, transformRedisJsonReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { ARRAPPEND, From 742d5713e8938be0f3b93adb2ddc858edf196ff4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 25 Jun 2025 13:15:44 +0300 Subject: [PATCH 12/19] fix(commands): sPopCount return Array (#3006) Also, touch the tests for spop and spopcount to use the new parseCommand API fixes #3004 --- packages/client/lib/commands/SPOP.spec.ts | 19 +++++++++++++++++-- .../client/lib/commands/SPOP_COUNT.spec.ts | 19 +++++++++++++++++-- packages/client/lib/commands/SPOP_COUNT.ts | 4 ++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/client/lib/commands/SPOP.spec.ts b/packages/client/lib/commands/SPOP.spec.ts index f435134416..542e1ba3fc 100644 --- a/packages/client/lib/commands/SPOP.spec.ts +++ b/packages/client/lib/commands/SPOP.spec.ts @@ -1,12 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP from './SPOP'; -import { parseArgs } from './generic-transformers'; +import { BasicCommandParser } from '../client/parser'; describe('SPOP', () => { it('transformArguments', () => { + const parser = new BasicCommandParser(); + SPOP.parseCommand(parser, 'key'); assert.deepEqual( - parseArgs(SPOP, 'key'), + parser.redisArgs, ['SPOP', 'key'] ); }); @@ -16,6 +18,19 @@ describe('SPOP', () => { await client.sPop('key'), null ); + + await client.sAdd('key', 'member'); + + assert.equal( + await client.sPop('key'), + 'member' + ); + + assert.equal( + await client.sPop('key'), + null + ); + }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN diff --git a/packages/client/lib/commands/SPOP_COUNT.spec.ts b/packages/client/lib/commands/SPOP_COUNT.spec.ts index 935ff43780..9720101f31 100644 --- a/packages/client/lib/commands/SPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts @@ -1,21 +1,36 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP_COUNT from './SPOP_COUNT'; -import { parseArgs } from './generic-transformers'; +import { BasicCommandParser } from '../client/parser'; describe('SPOP_COUNT', () => { it('transformArguments', () => { + const parser = new BasicCommandParser(); + SPOP_COUNT.parseCommand(parser, 'key', 1); assert.deepEqual( - parseArgs(SPOP_COUNT, 'key', 1), + parser.redisArgs, ['SPOP', 'key', '1'] ); }); testUtils.testAll('sPopCount', async client => { + assert.deepEqual( await client.sPopCount('key', 1), [] ); + + await Promise.all([ + client.sAdd('key', 'member'), + client.sAdd('key', 'member2'), + client.sAdd('key', 'member3') + ]) + + assert.deepEqual( + (await client.sPopCount('key', 3)).length, + 3 + ); + }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts index 1191f07cff..a285e6f5c4 100644 --- a/packages/client/lib/commands/SPOP_COUNT.ts +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -1,5 +1,5 @@ import { CommandParser } from '../client/parser'; -import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { RedisArgument, Command, ArrayReply } from '../RESP/types'; export default { IS_READ_ONLY: false, @@ -16,5 +16,5 @@ export default { parser.pushKey(key); parser.push(count.toString()); }, - transformReply: undefined as unknown as () => BlobStringReply | NullReply + transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; From 6f3380ba68786a3d5a9825f9a29c2053e43d32e3 Mon Sep 17 00:00:00 2001 From: Ricardo Ferreira Date: Wed, 25 Jun 2025 13:41:54 +0100 Subject: [PATCH 13/19] fix: ensure the repo links in the README are functional on the website (#3005) --- README.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 38615ee519..ab6b4707e6 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ npm install redis | Name | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------- | -| [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | -| [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | -| [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | -| [`@redis/json`](./packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | -| [`@redis/search`](./packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | -| [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | -| [`@redis/entraid`](./packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | +| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | > Looking for a high-level library to handle object mapping? > See [redis-om-node](https://github.com/redis/redis-om-node)! @@ -83,7 +83,7 @@ createClient({ ``` You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in -the [client configuration guide](./docs/client-configuration.md). +the [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md). To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it @@ -188,7 +188,7 @@ await pool.ping(); ### Pub/Sub -See the [Pub/Sub overview](./docs/pub-sub.md). +See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md). ### Scan Iterator @@ -234,7 +234,6 @@ of sending a `QUIT` command to the server, the client can simply close the netwo ```typescript client.destroy(); ``` - ### Client Side Caching Node Redis v5 adds support for [Client Side Caching](https://redis.io/docs/manual/client-side-caching/), which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid. @@ -251,7 +250,7 @@ const client = createClient({ }); ``` -See the [V5 documentation](./docs/v5.md#client-side-caching) for more details and advanced usage. +See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining @@ -275,11 +274,11 @@ await Promise.all([ ### Programmability -See the [Programmability overview](./docs/programmability.md). +See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md). ### Clustering -Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster. +Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster. ### Events @@ -292,12 +291,12 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | | `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | | `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | -| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | +| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | > :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and > an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. -> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. +> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions @@ -314,13 +313,13 @@ Node Redis is supported with the following versions of Redis: ## Migration -- [From V3 to V4](docs/v3-to-v4.md) -- [From V4 to V5](docs/v4-to-v5.md) -- [V5](docs/v5.md) +- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md) +- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md) +- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md) ## Contributing -If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md). Thank you to all the people who already contributed to Node Redis! @@ -328,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis! ## License -This repository is licensed under the "MIT" license. See [LICENSE](LICENSE). +This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE). From 79749f24618baa7d977470372cafaa4b747f0837 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 7 Jul 2025 11:06:34 +0300 Subject: [PATCH 14/19] fix(sentinel): propagate RESP option to clients (#3011) `createSentinel` takes RESP as an option, but does not propagate down to the actual clients. This creates confusion for the users as they expect the option to be set to all clients, which is reasonable. In case of clientSideCaching, this problem manifests as validation failure because clientSideCaching requires RESP3, but if we dont propagate, clients start with the default RESP2 fixes #3010 --- packages/client/lib/sentinel/index.spec.ts | 23 ++++++++++++++-------- packages/client/lib/sentinel/index.ts | 5 +++++ packages/test-utils/lib/index.ts | 2 ++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a9bdc8cc95..ef1702eab1 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -23,7 +23,7 @@ describe('RedisSentinel', () => { { host: 'localhost', port: 26379 } ] }; - + it('should throw error when clientSideCache is enabled with RESP 2', () => { assert.throws( () => RedisSentinel.create({ @@ -46,7 +46,7 @@ describe('RedisSentinel', () => { }); it('should not throw when clientSideCache is enabled with RESP 3', () => { - assert.doesNotThrow(() => + assert.doesNotThrow(() => RedisSentinel.create({ ...options, clientSideCache: clientSideCacheConfig, @@ -54,6 +54,16 @@ describe('RedisSentinel', () => { }) ); }); + + testUtils.testWithClientSentinel('should successfully connect to sentinel', async () => { + }, { + ...GLOBAL.SENTINEL.OPEN, + sentinelOptions: { + RESP: 3, + clientSideCache: { ttl: 0, maxEntries: 0, evictPolicy: 'LRU'}, + }, + }) + }); }); }); @@ -417,7 +427,7 @@ async function steadyState(frame: SentinelFramework) { sentinel.setTracer(tracer); await sentinel.connect(); await nodePromise; - + await sentinel.flushAll(); } finally { if (sentinel !== undefined) { @@ -443,7 +453,7 @@ describe('legacy tests', () => { this.timeout(15000); last = Date.now(); - + function deltaMeasurer() { const delta = Date.now() - last; if (delta > longestDelta) { @@ -508,7 +518,7 @@ describe('legacy tests', () => { } stopMeasuringBlocking = true; - + await frame.cleanup(); }) @@ -1032,6 +1042,3 @@ describe('legacy tests', () => { }) }); }); - - - diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index ec570e64bf..b4a794b871 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -625,6 +625,7 @@ class RedisSentinelInternal< readonly #sentinelClientOptions: RedisClientOptions; readonly #scanInterval: number; readonly #passthroughClientErrorEvents: boolean; + readonly #RESP?: RespVersions; #anotherReset = false; @@ -673,6 +674,7 @@ class RedisSentinelInternal< this.#name = options.name; + this.#RESP = options.RESP; this.#sentinelRootNodes = Array.from(options.sentinelRootNodes); this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16; this.#masterPoolSize = options.masterPoolSize ?? 1; @@ -716,6 +718,9 @@ class RedisSentinelInternal< #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) { return RedisClient.create({ + //first take the globally set RESP + RESP: this.#RESP, + //then take the client options, which can in theory overwrite it ...clientOptions, socket: { ...clientOptions.socket, diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index a41f970e0c..43dd4debfd 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -337,6 +337,7 @@ export default class TestUtils { port: promise.port })); + const sentinel = createSentinel({ name: 'mymaster', sentinelRootNodes: rootNodes, @@ -352,6 +353,7 @@ export default class TestUtils { functions: options?.functions || {}, masterPoolSize: options?.masterPoolSize || undefined, reserveClient: options?.reserveClient || false, + ...options?.sentinelOptions }) as RedisSentinelType; if (options.disableClientSetup) { From 65a12d50e711d86c6172f586b6a82c33efaf4bb2 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 7 Jul 2025 11:37:08 +0300 Subject: [PATCH 15/19] feat(client): add command timeout option (#3008) Co-authored-by: Florian Schunk <149071178+florian-schunk@users.noreply.github.com> --- docs/command-options.md | 13 ++ packages/client/lib/client/commands-queue.ts | 56 +++++++-- packages/client/lib/client/index.spec.ts | 118 +++++++++++++++---- packages/client/lib/client/index.ts | 12 +- packages/client/lib/cluster/index.ts | 28 +++-- packages/client/lib/sentinel/index.ts | 38 +++--- packages/client/lib/sentinel/utils.ts | 2 +- packages/test-utils/lib/index.ts | 21 ++-- 8 files changed, 212 insertions(+), 76 deletions(-) diff --git a/docs/command-options.md b/docs/command-options.md index b246445ad7..8583eae135 100644 --- a/docs/command-options.md +++ b/docs/command-options.md @@ -37,6 +37,19 @@ try { } ``` + +## Timeout + +This option is similar to the Abort Signal one, but provides an easier way to set timeout for commands. Again, this applies to commands that haven't been written to the socket yet. + +```javascript +const client = createClient({ + commandOptions: { + timeout: 1000 + } +}) +``` + ## ASAP Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue. diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index 78c0a01b20..52a07a7e3b 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -3,7 +3,7 @@ import encodeCommand from '../RESP/encoder'; import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder'; import { TypeMapping, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types'; import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub'; -import { AbortError, ErrorReply } from '../errors'; +import { AbortError, ErrorReply, TimeoutError } from '../errors'; import { MonitorCallback } from '.'; export interface CommandOptions { @@ -14,6 +14,10 @@ export interface CommandOptions { * Maps between RESP and JavaScript types */ typeMapping?: T; + /** + * Timeout for the command in milliseconds + */ + timeout?: number; } export interface CommandToWrite extends CommandWaitingForReply { @@ -23,6 +27,10 @@ export interface CommandToWrite extends CommandWaitingForReply { signal: AbortSignal; listener: () => unknown; } | undefined; + timeout: { + signal: AbortSignal; + listener: () => unknown; + } | undefined; } interface CommandWaitingForReply { @@ -80,7 +88,7 @@ export default class RedisCommandsQueue { #onPush(push: Array) { // TODO: type if (this.#pubSub.handleMessageReply(push)) return true; - + const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(push); if (isShardedUnsubscribe && !this.#waitingForReply.length) { const channel = push[1].toString(); @@ -153,12 +161,26 @@ export default class RedisCommandsQueue { args, chainId: options?.chainId, abort: undefined, + timeout: undefined, resolve, reject, channelsCounter: undefined, typeMapping: options?.typeMapping }; + const timeout = options?.timeout; + if (timeout) { + const signal = AbortSignal.timeout(timeout); + value.timeout = { + signal, + listener: () => { + this.#toWrite.remove(node); + value.reject(new TimeoutError()); + } + }; + signal.addEventListener('abort', value.timeout.listener, { once: true }); + } + const signal = options?.abortSignal; if (signal) { value.abort = { @@ -181,6 +203,7 @@ export default class RedisCommandsQueue { args: command.args, chainId, abort: undefined, + timeout: undefined, resolve() { command.resolve(); resolve(); @@ -202,7 +225,7 @@ export default class RedisCommandsQueue { this.decoder.onReply = (reply => { if (Array.isArray(reply)) { if (this.#onPush(reply)) return; - + if (PONG.equals(reply[0] as Buffer)) { const { resolve, typeMapping } = this.#waitingForReply.shift()!, buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer; @@ -250,7 +273,7 @@ export default class RedisCommandsQueue { if (!this.#pubSub.isActive) { this.#resetDecoderCallbacks(); } - + resolve(); }; } @@ -299,6 +322,7 @@ export default class RedisCommandsQueue { args: ['MONITOR'], chainId: options?.chainId, abort: undefined, + timeout: undefined, // using `resolve` instead of using `.then`/`await` to make sure it'll be called before processing the next reply resolve: () => { // after running `MONITOR` only `MONITOR` and `RESET` replies are expected @@ -317,7 +341,7 @@ export default class RedisCommandsQueue { reject, channelsCounter: undefined, typeMapping - }, options?.asap); + }, options?.asap); }); } @@ -340,11 +364,11 @@ export default class RedisCommandsQueue { this.#resetDecoderCallbacks(); this.#resetFallbackOnReply = undefined; this.#pubSub.reset(); - + this.#waitingForReply.shift()!.resolve(reply); return; } - + this.#resetFallbackOnReply!(reply); }) as Decoder['onReply']; @@ -352,6 +376,7 @@ export default class RedisCommandsQueue { args: ['RESET'], chainId, abort: undefined, + timeout: undefined, resolve, reject, channelsCounter: undefined, @@ -376,16 +401,20 @@ export default class RedisCommandsQueue { continue; } - // TODO reuse `toSend` or create new object? + // TODO reuse `toSend` or create new object? (toSend as any).args = undefined; if (toSend.abort) { RedisCommandsQueue.#removeAbortListener(toSend); toSend.abort = undefined; } + if (toSend.timeout) { + RedisCommandsQueue.#removeTimeoutListener(toSend); + toSend.timeout = undefined; + } this.#chainInExecution = toSend.chainId; toSend.chainId = undefined; this.#waitingForReply.push(toSend); - + yield encoded; toSend = this.#toWrite.shift(); } @@ -402,11 +431,18 @@ export default class RedisCommandsQueue { command.abort!.signal.removeEventListener('abort', command.abort!.listener); } + static #removeTimeoutListener(command: CommandToWrite) { + command.timeout!.signal.removeEventListener('abort', command.timeout!.listener); + } + static #flushToWrite(toBeSent: CommandToWrite, err: Error) { if (toBeSent.abort) { RedisCommandsQueue.#removeAbortListener(toBeSent); } - + if (toBeSent.timeout) { + RedisCommandsQueue.#removeTimeoutListener(toBeSent); + } + toBeSent.reject(err); } diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 4f752210db..f04d646706 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1,9 +1,9 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisClient, { RedisClientOptions, RedisClientType } from '.'; -import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors'; +import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, TimeoutError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; -import { spy } from 'sinon'; +import { spy, stub } from 'sinon'; import { once } from 'node:events'; import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec'; import { RESP_TYPES } from '../RESP/decoder'; @@ -239,30 +239,84 @@ describe('Client', () => { assert.equal(await client.sendCommand(['PING']), 'PONG'); }, GLOBAL.SERVERS.OPEN); - describe('AbortController', () => { - before(function () { - if (!global.AbortController) { - this.skip(); - } + testUtils.testWithClient('Unactivated AbortController should not abort', async client => { + await client.sendCommand(['PING'], { + abortSignal: new AbortController().signal }); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('success', async client => { - await client.sendCommand(['PING'], { - abortSignal: new AbortController().signal - }); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('AbortError', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.sendCommand(['PING'], { + abortSignal: AbortSignal.timeout(5) + }), AbortError); + }) + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('AbortError', client => { - const controller = new AbortController(); - controller.abort(); + testUtils.testWithClient('Timeout with custom timeout config', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.sendCommand(['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.SERVERS.OPEN); - return assert.rejects( - client.sendCommand(['PING'], { - abortSignal: controller.signal - }), - AbortError - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithCluster('Timeout with custom timeout config (cluster)', async cluster => { + await blockSetImmediate(async () => { + await assert.rejects(cluster.sendCommand(undefined, true, ['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithClientSentinel('Timeout with custom timeout config (sentinel)', async sentinel => { + await blockSetImmediate(async () => { + await assert.rejects(sentinel.sendCommand(true, ['PING'], { + timeout: 5 + }), TimeoutError); + }) + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithClient('Timeout with global timeout config', async client => { + await blockSetImmediate(async () => { + await assert.rejects(client.ping(), TimeoutError); + await assert.rejects(client.sendCommand(['PING']), TimeoutError); + }); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + commandOptions: { + timeout: 5 + } + } + }); + + testUtils.testWithCluster('Timeout with global timeout config (cluster)', async cluster => { + await blockSetImmediate(async () => { + await assert.rejects(cluster.HSET('key', 'foo', 'value'), TimeoutError); + await assert.rejects(cluster.sendCommand(undefined, true, ['PING']), TimeoutError); + }); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + commandOptions: { + timeout: 5 + } + } + }); + + testUtils.testWithClientSentinel('Timeout with global timeout config (sentinel)', async sentinel => { + await blockSetImmediate(async () => { + await assert.rejects(sentinel.HSET('key', 'foo', 'value'), TimeoutError); + await assert.rejects(sentinel.sendCommand(true, ['PING']), TimeoutError); + }); + }, { + ...GLOBAL.SENTINEL.OPEN, + clientOptions: { + commandOptions: { + timeout: 5 + } + } }); testUtils.testWithClient('undefined and null should not break the client', async client => { @@ -900,3 +954,23 @@ describe('Client', () => { }, GLOBAL.SERVERS.OPEN); }); }); + +/** + * Executes the provided function in a context where setImmediate is stubbed to not do anything. + * This blocks setImmediate callbacks from executing + */ +async function blockSetImmediate(fn: () => Promise) { + let setImmediateStub: any; + + try { + setImmediateStub = stub(global, 'setImmediate'); + setImmediateStub.callsFake(() => { + //Dont call the callback, effectively blocking execution + }); + await fn(); + } finally { + if (setImmediateStub) { + setImmediateStub.restore(); + } + } +} diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index a446ad8e75..128dc59967 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -526,7 +526,7 @@ export default class RedisClient< async #handshake(chainId: symbol, asap: boolean) { const promises = []; const commandsWithErrorHandlers = await this.#getHandshakeCommands(); - + if (asap) commandsWithErrorHandlers.reverse() for (const { cmd, errorHandler } of commandsWithErrorHandlers) { @@ -632,7 +632,7 @@ export default class RedisClient< // since they could be connected to an older version that doesn't support them. } }); - + commands.push({ cmd: [ 'CLIENT', @@ -889,7 +889,13 @@ export default class RedisClient< return Promise.reject(new ClientOfflineError()); } - const promise = this._self.#queue.addCommand(args, options); + // Merge global options with provided options + const opts = { + ...this._self._commandOptions, + ...options + } + + const promise = this._self.#queue.addCommand(args, opts); this._self.#scheduleWrite(); return promise; } diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index c2c251810e..6d26ac98c9 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -38,12 +38,12 @@ export interface RedisClusterOptions< // POLICIES extends CommandPolicies = CommandPolicies > extends ClusterCommander { /** - * Should contain details for some of the cluster nodes that the client will use to discover + * Should contain details for some of the cluster nodes that the client will use to discover * the "cluster topology". We recommend including details for at least 3 nodes here. */ rootNodes: Array; /** - * Default values used for every client in the cluster. Use this to specify global values, + * Default values used for every client in the cluster. Use this to specify global values, * for example: ACL credentials, timeouts, TLS configuration etc. */ defaults?: Partial; @@ -68,13 +68,13 @@ export interface RedisClusterOptions< nodeAddressMap?: NodeAddressMap; /** * Client Side Caching configuration for the pool. - * - * Enables Redis Servers and Clients to work together to cache results from commands + * + * Enables Redis Servers and Clients to work together to cache results from commands * sent to a server. The server will notify the client when cached results are no longer valid. * In pooled mode, the cache is shared across all clients in the pool. - * + * * Note: Client Side Caching is only supported with RESP3. - * + * * @example Anonymous cache configuration * ``` * const client = createCluster({ @@ -86,7 +86,7 @@ export interface RedisClusterOptions< * minimum: 5 * }); * ``` - * + * * @example Using a controllable cache * ``` * const cache = new BasicPooledClientSideCache({ @@ -406,7 +406,7 @@ export default class RedisCluster< proxy._commandOptions[key] = value; return proxy as RedisClusterType< M, - F, + F, S, RESP, K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING @@ -489,7 +489,7 @@ export default class RedisCluster< myFn = this._handleAsk(fn); continue; } - + if (err.message.startsWith('MOVED')) { await this._slots.rediscover(client); client = await this._slots.getClient(firstKey, isReadonly); @@ -497,7 +497,7 @@ export default class RedisCluster< } throw err; - } + } } } @@ -508,10 +508,16 @@ export default class RedisCluster< options?: ClusterCommandOptions, // defaultPolicies?: CommandPolicies ): Promise { + + // Merge global options with local options + const opts = { + ...this._self._commandOptions, + ...options + } return this._self._execute( firstKey, isReadonly, - options, + opts, (client, opts) => client.sendCommand(args, opts) ); } diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index b4a794b871..b3f3bbf0b8 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -35,7 +35,7 @@ export class RedisSentinelClient< /** * Indicates if the client connection is open - * + * * @returns `true` if the client connection is open, `false` otherwise */ @@ -45,7 +45,7 @@ export class RedisSentinelClient< /** * Indicates if the client connection is ready to accept commands - * + * * @returns `true` if the client connection is ready, `false` otherwise */ get isReady() { @@ -54,7 +54,7 @@ export class RedisSentinelClient< /** * Gets the command options configured for this client - * + * * @returns The command options for this client or `undefined` if none were set */ get commandOptions() { @@ -241,10 +241,10 @@ export class RedisSentinelClient< /** * Releases the client lease back to the pool - * + * * After calling this method, the client instance should no longer be used as it * will be returned to the client pool and may be given to other operations. - * + * * @returns A promise that resolves when the client is ready to be reused, or undefined * if the client was immediately ready * @throws Error if the lease has already been released @@ -274,7 +274,7 @@ export default class RedisSentinel< /** * Indicates if the sentinel connection is open - * + * * @returns `true` if the sentinel connection is open, `false` otherwise */ get isOpen() { @@ -283,7 +283,7 @@ export default class RedisSentinel< /** * Indicates if the sentinel connection is ready to accept commands - * + * * @returns `true` if the sentinel connection is ready, `false` otherwise */ get isReady() { @@ -554,15 +554,15 @@ export default class RedisSentinel< /** * Acquires a master client lease for exclusive operations - * + * * Used when multiple commands need to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`). * The returned client must be released after use with the `release()` method. - * + * * @returns A promise that resolves to a Redis client connected to the master node * @example * ```javascript * const clientLease = await sentinel.acquire(); - * + * * try { * await clientLease.watch('key'); * const resp = await clientLease.multi() @@ -671,7 +671,7 @@ class RedisSentinelInternal< super(); this.#validateOptions(options); - + this.#name = options.name; this.#RESP = options.RESP; @@ -733,7 +733,7 @@ class RedisSentinelInternal< /** * Gets a client lease from the master client pool - * + * * @returns A client info object or a promise that resolves to a client info object * when a client becomes available */ @@ -748,10 +748,10 @@ class RedisSentinelInternal< /** * Releases a client lease back to the pool - * + * * If the client was used for a transaction that might have left it in a dirty state, * it will be reset before being returned to the pool. - * + * * @param clientInfo The client info object representing the client to release * @returns A promise that resolves when the client is ready to be reused, or undefined * if the client was immediately ready or no longer exists @@ -791,10 +791,10 @@ class RedisSentinelInternal< async #connect() { let count = 0; - while (true) { + while (true) { this.#trace("starting connect loop"); - count+=1; + count+=1; if (this.#destroy) { this.#trace("in #connect and want to destroy") return; @@ -847,7 +847,7 @@ class RedisSentinelInternal< try { /* - // force testing of READONLY errors + // force testing of READONLY errors if (clientInfo !== undefined) { if (Math.floor(Math.random() * 10) < 1) { console.log("throwing READONLY error"); @@ -861,7 +861,7 @@ class RedisSentinelInternal< throw err; } - /* + /* rediscover and retry if doing a command against a "master" a) READONLY error (topology has changed) but we haven't been notified yet via pubsub b) client is "not ready" (disconnected), which means topology might have changed, but sentinel might not see it yet @@ -1574,4 +1574,4 @@ export class RedisSentinelFactory extends EventEmitter { } }); } -} \ No newline at end of file +} diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index 7e2404c2f7..c124981e25 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -6,7 +6,7 @@ import { NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, Pr /* TODO: should use map interface, would need a transform reply probably? as resp2 is list form, which this depends on */ export function parseNode(node: Record): RedisNode | undefined{ - + if (node.flags.includes("s_down") || node.flags.includes("disconnected") || node.flags.includes("failover_in_progress")) { return undefined; } diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 43dd4debfd..aab1c700f5 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -179,7 +179,7 @@ export default class TestUtils { this.#VERSION_NUMBERS = numbers; this.#DOCKER_IMAGE = { image: dockerImageName, - version: string, + version: string, mode: "server" }; } @@ -315,7 +315,7 @@ export default class TestUtils { if (passIndex != 0) { password = options.serverArguments[passIndex]; } - + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { const dockerImage = this.#DOCKER_IMAGE; before(function () { @@ -333,18 +333,19 @@ export default class TestUtils { const promises = await dockerPromises; const rootNodes: Array = promises.map(promise => ({ - host: "127.0.0.1", + host: "127.0.0.1", port: promise.port })); const sentinel = createSentinel({ - name: 'mymaster', - sentinelRootNodes: rootNodes, - nodeClientOptions: { + name: 'mymaster', + sentinelRootNodes: rootNodes, + nodeClientOptions: { + commandOptions: options.clientOptions?.commandOptions, password: password || undefined, }, - sentinelClientOptions: { + sentinelClientOptions: { password: password || undefined, }, replicaPoolSize: options?.replicaPoolSize || 0, @@ -507,7 +508,7 @@ export default class TestUtils { it(title, async function () { if (!dockersPromise) return this.skip(); - + const dockers = await dockersPromise, cluster = createCluster({ rootNodes: dockers.map(({ port }) => ({ @@ -580,12 +581,12 @@ export default class TestUtils { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir)) - + if (tmpDir) { fs.rmSync(tmpDir, { recursive: true }); } } - + return sentinels } } From d9a6bb376f9e6a54959c4424aad2e4c636071164 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 8 Jul 2025 14:28:50 +0300 Subject: [PATCH 16/19] chore(release): use deploy keys for relese (#3013) main branch is protected and does not allow direct pushes. the release action needs to push. branch protection rules can be bypassed for people and apps, but not github actions. one of the workarounds is to use a ruleset in which we set a deploy key see: https://github.com/orgs/community/discussions/25305\#discussioncomment-10728028 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5732f2eda..e7c9d58fe7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ssh-key: ${{ secrets.RELEASE_KEY }} - name: Setup Node.js uses: actions/setup-node@v4 From 748cad2e7f71df66e8787d78720f02a1b394ad82 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:25 +0000 Subject: [PATCH 17/19] Release client@5.6.0 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9fc9f93f9..f17556e3a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", + "integrity": "sha512-M3Svdwt6oSfyfQdqEr0L2HOJH2vK7GgCFx1NfAQvpWAT4+ljoT1L5S5cKT3dA9NJrxrOPDkdoTPWJnIrGCOcmw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/client/package.json b/packages/client/package.json index b95d1087d0..ee98d77ca1 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From b7a5f40ab154dd293f630af90f1370b2e6a836e2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:32 +0000 Subject: [PATCH 18/19] Release bloom@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f17556e3a0..fb9d1d164a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.5.6.tgz", + "integrity": "sha512-bNR3mxkwtfuCxNOzfV8B3R5zA1LiN57EH6zK4jVBIgzMzliNuReZXBFGnXvsi80/SYohajn78YdpYI+XNpqL+A==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 83dd6f893f..47d1f6978e 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*" From dab595d8d9a70d3efe957e1ac06b6f6639ecb8d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jul 2025 11:37:38 +0000 Subject: [PATCH 19/19] Release json@5.6.0 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb9d1d164a..cd844a0de5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.5.6.tgz", + "integrity": "sha512-AIsoe3SsGQagqAmSQHaqxEinm5oCWr7zxPWL90kKaEdLJ+zw8KBznf2i9oK0WUFP5pFssSQUXqnscQKe2amfDQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "packages/search": { "name": "@redis/search", "version": "5.5.6", diff --git a/packages/json/package.json b/packages/json/package.json index 0d77a3bc6b..edfdaec8ef 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.5.6", + "version": "5.6.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.5.6" + "@redis/client": "^5.6.0" }, "devDependencies": { "@redis/test-utils": "*"