From 6d032ed7d00cbbe04be526600f38f36a77cbb997 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 16 Jul 2024 10:52:50 -0700 Subject: [PATCH 01/26] Java Update README (#1944) Update REDME Signed-off-by: Yury-Fridlyand Signed-off-by: Chloe Yip --- java/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/java/README.md b/java/README.md index 26950765cb..aaa29dd0be 100644 --- a/java/README.md +++ b/java/README.md @@ -66,22 +66,22 @@ Gradle: ```groovy // osx-aarch_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: 'osx-aarch_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'osx-aarch_64' } // osx-x86_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: 'osx-x86_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'osx-x86_64' } // linux-aarch_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: 'linux-aarch_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-aarch_64' } // linux-x86_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: 'linux-x86_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-x86_64' } // with osdetector @@ -89,7 +89,7 @@ plugins { id "com.google.osdetector" version "1.7.3" } dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: osdetector.classifier + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: osdetector.classifier } ``` @@ -102,7 +102,7 @@ Maven: io.valkey valkey-glide osx-aarch_64 - 1.0.0 + 1.0.1 @@ -110,7 +110,7 @@ Maven: io.valkey valkey-glide osx-x86_64 - 1.0.0 + 1.0.1 @@ -118,7 +118,7 @@ Maven: io.valkey valkey-glide linux-aarch_64 - 1.0.0 + 1.0.1 @@ -126,7 +126,7 @@ Maven: io.valkey valkey-glide linux-x86_64 - 1.0.0 + 1.0.1 ``` From 0a9ef941ffea5066f42597388e25a8a18acbd055 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 12:17:52 -0700 Subject: [PATCH 02/26] current progress Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 23 ++++++++++++++++++++++- node/src/Transaction.ts | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 185265a5ca..0dfe27e338 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -33,6 +33,7 @@ import { createExpire, createExpireAt, createGet, + createGetDel, createHDel, createHExists, createHGet, @@ -82,6 +83,7 @@ import { createSMove, createSPop, createSRem, + createSUnion, createSUnionStore, createSet, createStrlen, @@ -106,7 +108,6 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, - createSUnion, } from "./Commands"; import { ClosingError, @@ -482,6 +483,26 @@ export class BaseClient { return this.createWritePromise(createGet(key)); } + /** + * Gets a string value associated with the given `key`and deletes the key. + * + * See https://valkey.io/commands/getdel/ for details. + * + * @param key - The key to retrieve from the database. + * @returns - If `key` exists, returns the `value` of `key`. Otherwise, return `null`. + * + * @example + * ```typescript + * const result = client.getdel("key"); + * console.log(result); //Output: 'value' + * + * const value = client.getdel("key"); // value is null + * ``` + */ + public getdel(key: string): Promise { + return this.createWritePromise(createGetDel(key)); + } + /** Set the given key with the given value. Return value is dependent on the passed options. * See https://valkey.io/commands/set/ for details. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 10cde919f0..c753d1706d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -34,6 +34,7 @@ import { createExpire, createExpireAt, createGet, + createGetDel, createHDel, createHExists, createHGet, @@ -175,6 +176,19 @@ export class BaseTransaction> { return this.addAndReturn(createGet(key)); } + /** + * Gets a string value associated with the given `key`and deletes the key. + * + * See https://valkey.io/commands/getdel/ for details. + * + * @param key - The key to retrieve from the database. + * + * Command Response - If `key` exists, returns the `value` of `key`. Otherwise, return `null`. + */ + public getdel(key: string): T { + return this.addAndReturn(createGetDel(key)); + } + /** Set the given key with the given value. Return value is dependent on the passed options. * See https://valkey.io/commands/set/ for details. * From 7c1833faf07175d6e7cbfed64a876e2e5bb817f6 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 15:01:09 -0700 Subject: [PATCH 03/26] implement getdel Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 2 +- node/src/Commands.ts | 7 +++++++ node/tests/SharedTests.ts | 21 +++++++++++++++++++++ node/tests/TestUtilities.ts | 4 ++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0dfe27e338..8455280f5f 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -494,7 +494,7 @@ export class BaseClient { * @example * ```typescript * const result = client.getdel("key"); - * console.log(result); //Output: 'value' + * console.log(result); // Output: 'value' * * const value = client.getdel("key"); // value is null * ``` diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8d2f6608cd..219f416f84 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -91,6 +91,13 @@ export function createGet(key: string): command_request.Command { return createCommand(RequestType.Get, [key]); } +/** + * @internal + */ +export function createGetDel(key: string): command_request.Command { + return createCommand(RequestType.GetDel, [key]); +} + export type SetOptions = { /** * `onlyIfDoesNotExist` - Only set the key if it does not already exist. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 7de522528c..4fb9d6f150 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -13,6 +13,7 @@ import { InfoOptions, InsertPosition, ProtocolVersion, + RequestError, Script, parseInfoResponse, } from "../"; @@ -509,6 +510,26 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `getdel test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const value1 = uuidv4(); + const key2 = uuidv4(); + + expect(await client.set(key1, value1)).toEqual("OK"); + checkSimple(await client.getdel(key1)).toEqual(value1); + expect(await client.getdel(key1)).toEqual(null); + + // key isn't a string + expect(await client.sadd(key2, ["a"])).toEqual(1); + await expect(client.getdel(key2)).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `testing hset and hget with multiple existing fields and one non existing field_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 66f5c16acc..4f73257820 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -314,6 +314,10 @@ export async function transactionTest( const args: ReturnType[] = []; baseTransaction.set(key1, "bar"); args.push("OK"); + baseTransaction.getdel(key1); + args.push("bar"); + baseTransaction.set(key1, "bar"); + args.push("OK"); baseTransaction.objectEncoding(key1); args.push("embstr"); baseTransaction.type(key1); From 0d6f0646f6a2c838776718daf55c9ec11bd1c6c2 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 15:03:06 -0700 Subject: [PATCH 04/26] implement getdel Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + package.json | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a4396780fc..a4e6b664d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added GETDEL command ([]()) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) diff --git a/package.json b/package.json new file mode 100644 index 0000000000..f78d6d006e --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-tsdoc": "^0.3.0", + "prettier": "^3.3.2", + "typescript": "^5.5.3" + } +} From 768fa6334d48ce26e4a45dea1c7d656acecfd548 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 15:05:10 -0700 Subject: [PATCH 05/26] update changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e6b664d0..d57d9b8440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* Node: Added GETDEL command ([]()) +* Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) From 67f685ccd110b808d884e6f848f45169f421e758 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 20:26:41 -0700 Subject: [PATCH 06/26] removed json file Signed-off-by: Chloe Yip --- package.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 package.json diff --git a/package.json b/package.json deleted file mode 100644 index f78d6d006e..0000000000 --- a/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "dependencies": { - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-tsdoc": "^0.3.0", - "prettier": "^3.3.2", - "typescript": "^5.5.3" - } -} From 69c82657527a32c293a4762d956db52e5ae73d48 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 16:39:00 -0700 Subject: [PATCH 07/26] implement lset Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 25 ++++++++++++++++++ node/src/Commands.ts | 11 ++++++++ node/src/Transaction.ts | 19 ++++++++++++++ node/tests/SharedTests.ts | 52 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 5 files changed, 109 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8455280f5f..75321fabb9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -55,6 +55,7 @@ import { createLPush, createLRange, createLRem, + createLSet, createLTrim, createMGet, createMSet, @@ -1085,6 +1086,30 @@ export class BaseClient { return this.createWritePromise(createLLen(key)); } + /** + * Sets the list element at `index` to `element`. + * The index is zero-based, so `0` means the first element, `1` the second element and so on. + * Negative indices can be used to designate elements starting at the tail of + * the list. Here, `-1` means the last element, `-2` means the penultimate and so forth. + * + * See https://valkey.io/commands/lset/ for details. + * + * @param key - The key of the list. + * @param index - The index of the element in the list to be set. + * @param element - The new element to set at the specified index. + * @returns Always "OK". + * + * @example + * ```typescript + * // Example usage of the lset method + * const response = await client.lset("test_key", 1, "two"); + * console.log(response); // Output: 'OK' - Indicates that the second index of the list has been set to "two". + * ``` + */ + public lset(key: string, index: number, element: string): Promise<"OK"> { + return this.createWritePromise(createLSet(key, index, element)); + } + /** Trim an existing list so that it will contain only the specified range of elements specified. * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 219f416f84..8a0a6cfc68 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -521,6 +521,17 @@ export function createLLen(key: string): command_request.Command { return createCommand(RequestType.LLen, [key]); } +/** + * @internal + */ +export function createLSet( + key: string, + index: number, + element: string, +): command_request.Command { + return createCommand(RequestType.LSet, [key, index.toString(), element]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c753d1706d..8c4f9a6373 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -57,6 +57,7 @@ import { createLPush, createLRange, createLRem, + createLSet, createLTrim, createMGet, createMSet, @@ -599,6 +600,24 @@ export class BaseTransaction> { return this.addAndReturn(createLLen(key)); } + /** + * Sets the list element at `index` to `element`. + * The index is zero-based, so `0` means the first element, `1` the second element and so on. + * Negative indices can be used to designate elements starting at the tail of + * the list. Here, `-1` means the last element, `-2` means the penultimate and so forth. + * + * See https://valkey.io/commands/lset/ for details. + * + * @param key - The key of the list. + * @param index - The index of the element in the list to be set. + * @param element - The new element to set at the specified index. + * + * Command Response - Always "OK". + */ + public lset(key: string, index: number, element: string): T { + return this.addAndReturn(createLSet(key, index, element)); + } + /** Trim an existing list so that it will contain only the specified range of elements specified. * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 4fb9d6f150..690a5ef3dc 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -894,6 +894,58 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lset test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonExistingKey = uuidv4(); + const index = 0; + const oobIndex = 10; + const negativeIndex = -1; + const element = "zero"; + const lpushArgs = ["four", "three", "two", "one"]; + const expectedList = ["zero", "two", "three", "four"]; + const expectedList2 = ["zero", "two", "three", "zero"]; + + // key does not exist + await expect( + client.lset(nonExistingKey, index, element), + ).rejects.toThrow(); + + expect(await client.lpush(key, lpushArgs)).toEqual(4); + + // index out of range + await expect( + client.lset(key, oobIndex, element), + ).rejects.toThrow(); + + // assert lset result + checkSimple(await client.lset(key, index, element)).toEqual( + "OK", + ); + checkSimple(await client.lrange(key, 0, negativeIndex)).toEqual( + expectedList, + ); + + // assert lset with a negative index for the last element in the list + checkSimple( + await client.lset(key, negativeIndex, element), + ).toEqual("OK"); + checkSimple(await client.lrange(key, 0, negativeIndex)).toEqual( + expectedList2, + ); + + // assert lset against a non-list key + const nonListKey = "nonListKey"; + expect(await client.sadd(nonListKey, ["a"])).toEqual(1); + + await expect(client.lset(nonListKey, 0, "b")).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `ltrim with existing key and key that holds a value that is not a list_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4f73257820..ffebb72ca1 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -373,6 +373,8 @@ export async function transactionTest( args.push(1); baseTransaction.ltrim(key5, 0, 1); args.push("OK"); + baseTransaction.lset(key5, 0, field + "3"); + args.push("OK"); baseTransaction.lrange(key5, 0, -1); args.push([field + "3", field + "2"]); baseTransaction.lpopCount(key5, 2); From b8a37d43176fc9694c4fb6b17971d65ca02d869d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 16:58:25 -0700 Subject: [PATCH 08/26] add changelog and edited tests Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/tests/SharedTests.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d57d9b8440..a5999ce7ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ #### Changes * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 690a5ef3dc..4fbdf57505 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -911,14 +911,14 @@ export function runBaseTests(config: { // key does not exist await expect( client.lset(nonExistingKey, index, element), - ).rejects.toThrow(); + ).rejects.toThrow(RequestError); expect(await client.lpush(key, lpushArgs)).toEqual(4); // index out of range await expect( client.lset(key, oobIndex, element), - ).rejects.toThrow(); + ).rejects.toThrow(RequestError); // assert lset result checkSimple(await client.lset(key, index, element)).toEqual( @@ -940,7 +940,9 @@ export function runBaseTests(config: { const nonListKey = "nonListKey"; expect(await client.sadd(nonListKey, ["a"])).toEqual(1); - await expect(client.lset(nonListKey, 0, "b")).rejects.toThrow(); + await expect(client.lset(nonListKey, 0, "b")).rejects.toThrow( + RequestError, + ); }, protocol); }, config.timeout, From cb1c63f2b72a4468db655b902dcf71b8abdc7e92 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Wed, 17 Jul 2024 00:21:05 +0000 Subject: [PATCH 09/26] Updated attribution files Signed-off-by: ort-bot Signed-off-by: Chloe Yip --- glide-core/THIRD_PARTY_LICENSES_RUST | 2 +- java/THIRD_PARTY_LICENSES_JAVA | 2 +- node/THIRD_PARTY_LICENSES_NODE | 4 ++-- python/THIRD_PARTY_LICENSES_PYTHON | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index f60dec915d..0bc2e35712 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -25070,7 +25070,7 @@ the following restrictions: ---- -Package: tokio:1.38.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: diff --git a/java/THIRD_PARTY_LICENSES_JAVA b/java/THIRD_PARTY_LICENSES_JAVA index 54f97844d4..a363964613 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -25965,7 +25965,7 @@ the following restrictions: ---- -Package: tokio:1.38.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 4aff8bbe0b..8f750e1968 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -27153,7 +27153,7 @@ the following restrictions: ---- -Package: tokio:1.38.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: @@ -39964,7 +39964,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:20.14.10 +Package: @types:node:20.14.11 The following copyrights and licenses were found in the source code of this package: diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index f6a26f1c63..fe669c485d 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -27359,7 +27359,7 @@ the following restrictions: ---- -Package: tokio:1.38.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: From 9aa12afc7b8ff7eddf15d284acc36b798d5f3399 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 17 Jul 2024 10:05:44 -0700 Subject: [PATCH 10/26] Fixes for java client release pipeline (#1922) Signed-off-by: Yury-Fridlyand Signed-off-by: Chloe Yip --- .github/workflows/java-cd.yml | 21 +++++------------- .github/workflows/java.yml | 14 +++++------- examples/java/build.gradle | 2 +- java/DEVELOPER.md | 7 +++--- java/client/build.gradle | 42 +++++++++++++++++++++++++++-------- 5 files changed, 47 insertions(+), 39 deletions(-) diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml index 1dba670476..0859892b45 100644 --- a/.github/workflows/java-cd.yml +++ b/.github/workflows/java-cd.yml @@ -56,26 +56,21 @@ jobs: OS: ubuntu, RUNNER: ubuntu-latest, TARGET: x86_64-unknown-linux-gnu, - CLASSIFIER: linux-x86_64 } - { OS: ubuntu, RUNNER: ["self-hosted", "Linux", "ARM64"], TARGET: aarch64-unknown-linux-gnu, - CLASSIFIER: linux-aarch_64, - CONTAINER: "2_28" } - { OS: macos, RUNNER: macos-12, TARGET: x86_64-apple-darwin, - CLASSIFIER: osx-x86_64 } - { OS: macos, RUNNER: macos-latest, TARGET: aarch64-apple-darwin, - CLASSIFIER: osx-aarch_64 } runs-on: ${{ matrix.host.RUNNER }} @@ -99,7 +94,7 @@ jobs: - name: Set the release version shell: bash run: | - if ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}; then + if ${{ github.event_name == 'pull_request' }}; then R_VERSION="255.255.255" elif ${{ github.event_name == 'workflow_dispatch' }}; then R_VERSION="${{ env.INPUT_VERSION }}" @@ -139,18 +134,12 @@ jobs: env: SECRING_GPG: ${{ secrets.SECRING_GPG }} - - name: Replace placeholders and version in build.gradle - shell: bash - working-directory: ./java/client - run: | - SED_FOR_MACOS=`if [[ "${{ matrix.host.os }}" =~ .*"macos".* ]]; then echo "''"; fi` - sed -i $SED_FOR_MACOS 's/placeholder/${{ matrix.host.CLASSIFIER }}/g' build.gradle - sed -i $SED_FOR_MACOS "s/255.255.255/${{ env.RELEASE_VERSION }}/g" build.gradle - - name: Build java client working-directory: java run: | ./gradlew :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} + env: + GLIDE_RELEASE_VERSION: ${{ env.RELEASE_VERSION }} - name: Bundle JAR working-directory: java @@ -160,7 +149,7 @@ jobs: jar -cvf bundle.jar * ls -ltr cd - - cp $src_folder/bundle.jar . + cp $src_folder/bundle.jar bundle-${{ matrix.host.TARGET }}.jar - name: Upload artifacts to publish continue-on-error: true @@ -168,4 +157,4 @@ jobs: with: name: java-${{ matrix.host.TARGET }} path: | - java/bundle.jar + java/bundle*.jar diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 2e19a91a85..3b18254ded 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -97,7 +97,7 @@ jobs: - name: Build java client working-directory: java - run: ./gradlew --continue build + run: ./gradlew --continue build -x javadoc - name: Ensure no skipped files by linter working-directory: java @@ -165,22 +165,18 @@ jobs: - name: Install Java run: | - yum install -y java-${{ matrix.java }} + yum install -y java-${{ matrix.java }}-amazon-corretto-devel.x86_64 - - name: Build rust part + - name: Build java wrapper working-directory: java - run: cargo build --release - - - name: Build java part - working-directory: java - run: ./gradlew --continue build + run: ./gradlew --continue build -x javadoc - name: Upload test & spotbugs reports if: always() continue-on-error: true uses: actions/upload-artifact@v4 with: - name: test-reports-${{ matrix.java }} + name: test-reports-${{ matrix.java }}-amazon-linux path: | java/client/build/reports/** java/integTest/build/reports/** diff --git a/examples/java/build.gradle b/examples/java/build.gradle index be87b0a3f8..fa55ac434e 100644 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0', classifier: osdetector.classifier + implementation "io.valkey:valkey-glide:1.0.1:${osdetector.classifier}" } application { diff --git a/java/DEVELOPER.md b/java/DEVELOPER.md index d4dfd0ea82..413a90f953 100644 --- a/java/DEVELOPER.md +++ b/java/DEVELOPER.md @@ -189,11 +189,10 @@ dependencies { } ``` -Optionally: you can specify a snapshot release and classifier: +Optionally: you can specify a snapshot release: ```bash -export GLIDE_LOCAL_VERSION=1.0.0-SNAPSHOT -export GLIDE_LOCAL_CLASSIFIER=osx-aarch_64 +export GLIDE_RELEASE_VERSION=1.0.1-SNAPSHOT ./gradlew publishToMavenLocal ``` @@ -204,7 +203,7 @@ repositories { } dependencies { // Update to use version defined in the previous step - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.0-SNAPSHOT', classifier='osx-aarch_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1-SNAPSHOT', classifier='osx-aarch_64' } ``` diff --git a/java/client/build.gradle b/java/client/build.gradle index a1543bb85b..0178f311ea 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -4,7 +4,9 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id ("com.github.spotbugs") version "6.0.18" + id 'io.freefair.lombok' version '8.6' + id 'com.github.spotbugs' version '6.0.18' + id 'com.google.osdetector' version '1.7.3' } repositories { @@ -156,7 +158,11 @@ tasks.register('copyNativeLib', Copy) { into sourceSets.main.output.resourcesDir } +def defaultReleaseVersion = "255.255.255"; + +delombok.dependsOn('compileJava') jar.dependsOn('copyNativeLib') +javadoc.dependsOn('copyNativeLib') copyNativeLib.dependsOn('buildRustRelease') compileTestJava.dependsOn('copyNativeLib') test.dependsOn('buildRust') @@ -177,16 +183,13 @@ sourceSets { } } -// version is replaced during released workflow java-cd.yml -def defaultReleaseVersion = "255.255.255"; - publishing { publications { mavenJava(MavenPublication) { from components.java groupId = 'io.valkey' artifactId = 'valkey-glide' - version = System.getenv("GLIDE_LOCAL_VERSION") ?: defaultReleaseVersion; + version = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; pom { name = 'valkey-glide' description = 'General Language Independent Driver for the Enterprise (GLIDE) for Valkey' @@ -218,8 +221,14 @@ publishing { } } +java { + modularity.inferModulePath = true + withSourcesJar() + withJavadocJar() +} + tasks.withType(Sign) { - def releaseVersion = System.getenv("GLIDE_LOCAL_VERSION") ?: defaultReleaseVersion; + def releaseVersion = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; def isReleaseVersion = !releaseVersion.endsWith("SNAPSHOT") && releaseVersion != defaultReleaseVersion; onlyIf("isReleaseVersion is set") { isReleaseVersion } } @@ -239,9 +248,24 @@ tasks.withType(Test) { } jar { - archiveBaseName = "valkey-glide" - // placeholder will be renamed by platform+arch on the release workflow java-cd.yml - archiveClassifier = System.getenv("GLIDE_LOCAL_CLASSIFIER") ?: "placeholder" + archiveClassifier = osdetector.classifier +} + +sourcesJar { + // suppress following error + // Entry glide/api/BaseClient.java is a duplicate but no duplicate handling strategy has been set + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +delombok { + modulePath = classpath +} + +javadoc { + dependsOn delombok + source = delombok.outputs + options.tags = [ "example:a:Example:" ] + failOnError = false // TODO fix all javadoc errors and warnings and remove that } spotbugsMain { From 985d9505d236e45864ad14ab4ceaff9b461c3b95 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 21:53:22 -0700 Subject: [PATCH 11/26] write rpushx shared test Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 39 +++++++++++++++++++++++++++++++++++++++ node/src/Commands.ts | 20 ++++++++++++++++++++ node/src/Transaction.ts | 29 +++++++++++++++++++++++++++++ node/tests/SharedTests.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 75321fabb9..69aa18697a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -53,6 +53,7 @@ import { createLLen, createLPop, createLPush, + createLPushX, createLRange, createLRem, createLSet, @@ -71,6 +72,7 @@ import { createPfCount, createRPop, createRPush, + createRPushX, createRename, createRenameNX, createSAdd, @@ -977,6 +979,25 @@ export class BaseClient { return this.createWritePromise(createLPush(key, elements)); } + /** + * Inserts specified values at the head of the`list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/lpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the head of the list stored at `key`. + * @returns - The length of the list after the push operation. + * @example + * ```typescript + * const listLength = await client.lpushx("my_list", ["value1", "value2"]); + * console.log(result); // Output: 2 - Indicates that the list has two elements. + * ``` + */ + public lpushx(key: string, elements: string[]): Promise { + return this.createWritePromise(createLPushX(key, elements)); + } + /** Removes and returns the first elements of the list stored at `key`. * The command pops a single element from the beginning of the list. * See https://valkey.io/commands/lpop/ for details. @@ -1184,6 +1205,24 @@ export class BaseClient { return this.createWritePromise(createRPush(key, elements)); } + /** + * Inserts specified values at the tail of the `list`, only if `key` already + * exists and holds a list. + * See https://valkey.io/commands/rpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the tail of the list stored at `key`. + * @returns - The length of the list after the push operation. + * @example + * ```typescript + * const result = await client.rpushx("my_list", ["value1", "value2"]); + * console.log(result); // Output: 2 - Indicates that the list has two elements. + * ``` + * */ + public rpushx(key: string, elements: string[]): Promise { + return this.createWritePromise(createRPushX(key, elements)); + } + /** Removes and returns the last elements of the list stored at `key`. * The command pops a single element from the end of the list. * See https://valkey.io/commands/rpop/ for details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8a0a6cfc68..6919faa80e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -488,6 +488,16 @@ export function createLPush( return createCommand(RequestType.LPush, [key].concat(elements)); } +/** + * @internal + */ +export function createLPushX( + key: string, + elements: string[], +): command_request.Command { + return createCommand(RequestType.LPushX, [key].concat(elements)); +} + /** * @internal */ @@ -568,6 +578,16 @@ export function createRPush( return createCommand(RequestType.RPush, [key].concat(elements)); } +/** + * @internal + */ +export function createRPushX( + key: string, + elements: string[], +): command_request.Command { + return createCommand(RequestType.RPushX, [key].concat(elements)); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8c4f9a6373..b8cc6be557 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -55,6 +55,7 @@ import { createLLen, createLPop, createLPush, + createLPushX, createLRange, createLRem, createLSet, @@ -74,6 +75,7 @@ import { createPing, createRPop, createRPush, + createRPushX, createRename, createRenameNX, createSAdd, @@ -543,6 +545,20 @@ export class BaseTransaction> { return this.addAndReturn(createLPush(key, elements)); } + /** + * Inserts specified values at the head of the`list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/lpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the head of the list stored at `key`. + * @returns - The length of the list after the push operation. + */ + public lpushx(key: string, elements: string[]): T { + return this.addAndReturn(createLPushX(key, elements)); + } + /** Removes and returns the first elements of the list stored at `key`. * The command pops a single element from the beginning of the list. * See https://valkey.io/commands/lpop/ for details. @@ -667,6 +683,19 @@ export class BaseTransaction> { return this.addAndReturn(createRPush(key, elements)); } + /** + * Inserts specified values at the tail of the `list`, only if `key` already + * exists and holds a list. + * See https://valkey.io/commands/rpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the tail of the list stored at `key`. + * @returns - The length of the list after the push operation. + */ + public rpushx(key: string, elements: string[]): T { + return this.addAndReturn(createRPushX(key, elements)); + } + /** Removes and returns the last elements of the list stored at `key`. * The command pops a single element from the end of the list. * See https://valkey.io/commands/rpop/ for details. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 4fbdf57505..b5e572120b 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -868,6 +868,41 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lpushx list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const key3 = uuidv4(); + + expect(await client.lpush(key1, ["0"])).toEqual(1); + expect(await client.lpushx(key1, ["1", "2", "3"])).toEqual(4); + expect(await client.lrange(key1, 0, -1)).toEqual([ + "3", + "2", + "1", + "0", + ]); + + expect(await client.lpushx(key2, ["1"])).toEqual(0); + expect(await client.lrange(key2, 0, -1)).toEqual([]); + + // Key exists, but is not a list + checkSimple(await client.set(key3, "bar")); + await expect(client.lpushx(key3, "_")).rejects.toThrow( + RequestError, + ); + + // Empty element list + await expect(client.lpushx(key2, [])).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `llen with existing, non-existing key and key that holds a value that is not a list_%p`, async (protocol) => { From 454d67cde47a1f745f781f0473dca2394c4a3a6c Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:26:37 -0700 Subject: [PATCH 12/26] add rpushx test Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b5e572120b..7f1ac06ce2 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -878,7 +878,7 @@ export function runBaseTests(config: { expect(await client.lpush(key1, ["0"])).toEqual(1); expect(await client.lpushx(key1, ["1", "2", "3"])).toEqual(4); - expect(await client.lrange(key1, 0, -1)).toEqual([ + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ "3", "2", "1", @@ -886,11 +886,11 @@ export function runBaseTests(config: { ]); expect(await client.lpushx(key2, ["1"])).toEqual(0); - expect(await client.lrange(key2, 0, -1)).toEqual([]); + checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); // Key exists, but is not a list checkSimple(await client.set(key3, "bar")); - await expect(client.lpushx(key3, "_")).rejects.toThrow( + await expect(client.lpushx(key3, ["_"])).rejects.toThrow( RequestError, ); From ddd9b9af63fa7a0a56749fbd8f190c5c45550ae0 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:30:36 -0700 Subject: [PATCH 13/26] implement both shared command tests Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 7f1ac06ce2..2c879cd468 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1095,6 +1095,41 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `rpushx list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const key3 = uuidv4(); + + expect(await client.rpush(key1, ["0"])).toEqual(1); + expect(await client.rpushx(key1, ["1", "2", "3"])).toEqual(4); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "0", + "1", + "2", + "3", + ]); + + expect(await client.rpushx(key2, ["1"])).toEqual(0); + checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); + + // Key exists, but is not a list + checkSimple(await client.set(key3, "bar")); + await expect(client.rpushx(key3, ["_"])).rejects.toThrow( + RequestError, + ); + + // Empty element list + await expect(client.rpushx(key2, [])).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sadd, srem, scard and smembers with existing set_%p`, async (protocol) => { From 14f925969d8542f0dc8683f08085c12c9b9066e7 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:38:13 -0700 Subject: [PATCH 14/26] implement rpushx and lpushx Signed-off-by: Chloe Yip --- node/tests/TestUtilities.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ffebb72ca1..8341aec942 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -309,6 +309,7 @@ export async function transactionTest( const key12 = "{key}" + uuidv4(); const key13 = "{key}" + uuidv4(); const key14 = "{key}" + uuidv4(); // sorted set + const key15 = "{key}" + uuidv4(); // pushx const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -394,6 +395,10 @@ export async function transactionTest( args.push(field + "3"); baseTransaction.rpopCount(key6, 2); args.push([field + "2", field + "1"]); + baseTransaction.rpushx(key15, ["_"]); + args.push(0); + baseTransaction.lpushx(key15, ["_"]); + args.push(0); baseTransaction.sadd(key7, ["bar", "foo"]); args.push(2); baseTransaction.sunionstore(key7, [key7, key7]); From 8626a21d5527466a355c68749c95fd50a691acf3 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:40:32 -0700 Subject: [PATCH 15/26] updated changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5999ce7ec..8d58abc194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) From 6181c9890b79d1f6c42ca88d19872119248670ab Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 11:09:40 -0700 Subject: [PATCH 16/26] address comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 5 +++-- node/src/Transaction.ts | 9 ++++++--- node/tests/TestUtilities.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 69aa18697a..60c4dc3f2f 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -987,7 +987,7 @@ export class BaseClient { * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. - * @returns - The length of the list after the push operation. + * @returns The length of the list after the push operation. * @example * ```typescript * const listLength = await client.lpushx("my_list", ["value1", "value2"]); @@ -1208,11 +1208,12 @@ export class BaseClient { /** * Inserts specified values at the tail of the `list`, only if `key` already * exists and holds a list. + * * See https://valkey.io/commands/rpushx/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. - * @returns - The length of the list after the push operation. + * @returns The length of the list after the push operation. * @example * ```typescript * const result = await client.rpushx("my_list", ["value1", "value2"]); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b8cc6be557..613a622325 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -546,14 +546,15 @@ export class BaseTransaction> { } /** - * Inserts specified values at the head of the`list`, only if `key` already + * Inserts specified values at the head of the `list`, only if `key` already * exists and holds a list. * * See https://valkey.io/commands/lpushx/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. - * @returns - The length of the list after the push operation. + * + * Command Response - The length of the list after the push operation. */ public lpushx(key: string, elements: string[]): T { return this.addAndReturn(createLPushX(key, elements)); @@ -686,11 +687,13 @@ export class BaseTransaction> { /** * Inserts specified values at the tail of the `list`, only if `key` already * exists and holds a list. + * * See https://valkey.io/commands/rpushx/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. - * @returns - The length of the list after the push operation. + * + * Command Response - The length of the list after the push operation. */ public rpushx(key: string, elements: string[]): T { return this.addAndReturn(createRPushX(key, elements)); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 8341aec942..8480337719 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -309,7 +309,7 @@ export async function transactionTest( const key12 = "{key}" + uuidv4(); const key13 = "{key}" + uuidv4(); const key14 = "{key}" + uuidv4(); // sorted set - const key15 = "{key}" + uuidv4(); // pushx + const key15 = "{key}" + uuidv4(); // list const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -395,7 +395,7 @@ export async function transactionTest( args.push(field + "3"); baseTransaction.rpopCount(key6, 2); args.push([field + "2", field + "1"]); - baseTransaction.rpushx(key15, ["_"]); + baseTransaction.rpushx(key15, ["_"]); // key15 is empty args.push(0); baseTransaction.lpushx(key15, ["_"]); args.push(0); From 8e7d9eb21cc004caf882a6eb5508c762a730df36 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 11:37:24 -0700 Subject: [PATCH 17/26] addressed remaining comment Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 60c4dc3f2f..0ae6eff7fb 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -980,7 +980,7 @@ export class BaseClient { } /** - * Inserts specified values at the head of the`list`, only if `key` already + * Inserts specified values at the head of the `list`, only if `key` already * exists and holds a list. * * See https://valkey.io/commands/lpushx/ for details. From ed8aa27a0d8c22cfc4ff84c59902ae2e192471a8 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:14:00 -0700 Subject: [PATCH 18/26] Node: add SINTERCARD command (#1956) * Node: add SINTERCARD command Signed-off-by: aaron-congo Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 27 ++++++++++++ node/src/Commands.ts | 17 ++++++++ node/src/Transaction.ts | 16 ++++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 59 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 8 ++++ 7 files changed, 129 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d58abc194..e27cbcbd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) +* Node: Added SINTERCARD command ([#1956](https://github.com/valkey-io/valkey-glide/pull/1956)) * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) * Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0ae6eff7fb..2173fab4d0 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -80,6 +80,7 @@ import { createSDiff, createSDiffStore, createSInter, + createSInterCard, createSInterStore, createSIsMember, createSMembers, @@ -1406,6 +1407,32 @@ export class BaseClient { ); } + /** + * Gets the cardinality of the intersection of all the given sets. + * + * See https://valkey.io/commands/sintercard/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sets. + * @returns The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.sadd("set1", ["a", "b", "c"]); + * await client.sadd("set2", ["b", "c", "d"]); + * const result1 = await client.sintercard(["set1", "set2"]); + * console.log(result1); // Output: 2 - The intersection of "set1" and "set2" contains 2 elements: "b" and "c". + * + * const result2 = await client.sintercard(["set1", "set2"], 1); + * console.log(result2); // Output: 1 - The computation stops early as the intersection cardinality reaches the limit of 1. + * ``` + */ + public sintercard(keys: string[], limit?: number): Promise { + return this.createWritePromise(createSInterCard(keys, limit)); + } + /** * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 6919faa80e..31a40a58d1 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -652,6 +652,23 @@ export function createSInter(keys: string[]): command_request.Command { return createCommand(RequestType.SInter, keys); } +/** + * @internal + */ +export function createSInterCard( + keys: string[], + limit?: number, +): command_request.Command { + let args: string[] = keys; + args.unshift(keys.length.toString()); + + if (limit != undefined) { + args = args.concat(["LIMIT", limit.toString()]); + } + + return createCommand(RequestType.SInterCard, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 613a622325..7e30acafd2 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -83,6 +83,7 @@ import { createSDiff, createSDiffStore, createSInter, + createSInterCard, createSInterStore, createSIsMember, createSMembers, @@ -801,6 +802,21 @@ export class BaseTransaction> { return this.addAndReturn(createSInter(keys), true); } + /** + * Gets the cardinality of the intersection of all the given sets. + * + * See https://valkey.io/commands/sintercard/ for more details. + * + * @param keys - The keys of the sets. + * + * Command Response - The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. + * + * since Valkey version 7.0.0. + */ + public sintercard(keys: string[], limit?: number): T { + return this.addAndReturn(createSInterCard(keys, limit)); + } + /** * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index a7886fb3dc..734b98e006 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -300,6 +300,7 @@ describe("GlideClusterClient", () => { client.smove("abc", "zxy", "value"), client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), + client.sintercard(["abc", "zxy", "lkn"]), client.sinterstore("abc", ["zxy", "lkn"]), client.zinterstore("abc", ["zxy", "lkn"]), client.sunionstore("abc", ["zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 2c879cd468..d99ec53051 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1336,6 +1336,65 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sintercard test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("7.0.0")) { + return; + } + + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + const member1_list = ["a", "b", "c", "d"]; + const member2_list = ["b", "c", "d", "e"]; + + expect(await client.sadd(key1, member1_list)).toEqual(4); + expect(await client.sadd(key2, member2_list)).toEqual(4); + + expect(await client.sintercard([key1, key2])).toEqual(3); + + // returns limit as cardinality when the limit is reached partway through the computation + const limit = 2; + expect(await client.sintercard([key1, key2], limit)).toEqual( + limit, + ); + + // returns actual cardinality if limit is higher + expect(await client.sintercard([key1, key2], 4)).toEqual(3); + + // one of the keys is empty, intersection is empty, cardinality equals 0 + expect(await client.sintercard([key1, nonExistingKey])).toEqual( + 0, + ); + + expect( + await client.sintercard([nonExistingKey, nonExistingKey]), + ).toEqual(0); + expect( + await client.sintercard( + [nonExistingKey, nonExistingKey], + 2, + ), + ).toEqual(0); + + // invalid argument - key list must not be empty + await expect(client.sintercard([])).rejects.toThrow( + RequestError, + ); + + // source key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sintercard([key1, stringKey]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sinterstore test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 8480337719..d3c9c198a9 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -407,6 +407,14 @@ export async function transactionTest( args.push(new Set(["bar", "foo"])); baseTransaction.sinter([key7, key7]); args.push(new Set(["bar", "foo"])); + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.sintercard([key7, key7]); + args.push(2); + baseTransaction.sintercard([key7, key7], 1); + args.push(1); + } + baseTransaction.sinterstore(key7, [key7, key7]); args.push(2); baseTransaction.sdiff([key7, key7]); From 347b7436817de7e2ebb7d5d08b2029a6aeb6d248 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 17 Jul 2024 13:14:40 -0700 Subject: [PATCH 19/26] Node: Add LOLWUT command (#1934) * Node: Add LOLWUT command Signed-off-by: Yi-Pin Chen Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/Commands.ts | 32 +++++++++++++++ node/src/GlideClient.ts | 20 +++++++++ node/src/GlideClusterClient.ts | 28 +++++++++++++ node/src/Transaction.ts | 17 +++++++- node/tests/RedisClient.test.ts | 58 +++++++++++++++++++++++++++ node/tests/RedisClusterClient.test.ts | 58 +++++++++++++++++++++++++++ 7 files changed, 213 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27cbcbd14..0f92aaa5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) * Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924)) +* Node: Added LOLWUT command ([#1934](https://github.com/valkey-io/valkey-glide/pull/1934)) ## 1.0.0 (2024-07-09) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 31a40a58d1..0509a6f741 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1614,3 +1614,35 @@ export function createObjectIdletime(key: string): command_request.Command { export function createObjectRefcount(key: string): command_request.Command { return createCommand(RequestType.ObjectRefCount, [key]); } + +export type LolwutOptions = { + /** + * An optional argument that can be used to specify the version of computer art to generate. + */ + version?: number; + /** + * An optional argument that can be used to specify the output: + * For version `5`, those are length of the line, number of squares per row, and number of squares per column. + * For version `6`, those are number of columns and number of lines. + */ + parameters?: number[]; +}; + +/** + * @internal + */ +export function createLolwut(options?: LolwutOptions): command_request.Command { + const args: string[] = []; + + if (options) { + if (options.version !== undefined) { + args.push("VERSION", options.version.toString()); + } + + if (options.parameters !== undefined) { + args.push(...options.parameters.map((param) => param.toString())); + } + } + + return createCommand(RequestType.Lolwut, args); +} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index da9ac34b84..b7e76a4ba8 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -6,6 +6,7 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { InfoOptions, + LolwutOptions, createClientGetName, createClientId, createConfigGet, @@ -15,6 +16,7 @@ import { createCustomCommand, createEcho, createInfo, + createLolwut, createPing, createSelect, createTime, @@ -310,4 +312,22 @@ export class GlideClient extends BaseClient { public time(): Promise<[string, string]> { return this.createWritePromise(createTime()); } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options + * @returns A piece of generative computer art along with the current server version. + * + * @example + * ```typescript + * const response = await client.lolwut({ version: 6, parameters: [40, 20] }); + * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. + * ``` + */ + public lolwut(options?: LolwutOptions): Promise { + return this.createWritePromise(createLolwut(options)); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 0fda4cb2ac..663925aae7 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -6,6 +6,7 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { InfoOptions, + LolwutOptions, createClientGetName, createClientId, createConfigGet, @@ -15,6 +16,7 @@ import { createCustomCommand, createEcho, createInfo, + createLolwut, createPing, createTime, } from "./Commands"; @@ -567,4 +569,30 @@ export class GlideClusterClient extends BaseClient { public time(route?: Routes): Promise> { return this.createWritePromise(createTime(), toProtobufRoute(route)); } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options. + * @param route - The command will be routed to a random node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns A piece of generative computer art along with the current server version. + * + * @example + * ```typescript + * const response = await client.lolwut({ version: 6, parameters: [40, 20] }, "allNodes"); + * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. + * ``` + */ + public lolwut( + options?: LolwutOptions, + route?: Routes, + ): Promise> { + return this.createWritePromise( + createLolwut(options), + toProtobufRoute(route), + ); + } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7e30acafd2..890c8fe9a7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -8,6 +8,7 @@ import { InfoOptions, InsertPosition, KeyWeight, + LolwutOptions, RangeByIndex, RangeByLex, RangeByScore, @@ -60,6 +61,7 @@ import { createLRem, createLSet, createLTrim, + createLolwut, createMGet, createMSet, createObjectEncoding, @@ -90,6 +92,7 @@ import { createSMove, createSPop, createSRem, + createSUnion, createSUnionStore, createSelect, createSet, @@ -116,7 +119,6 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, - createSUnion, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1663,6 +1665,19 @@ export class BaseTransaction> { public objectRefcount(key: string): T { return this.addAndReturn(createObjectRefcount(key)); } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options. + * + * Command Response - A piece of generative computer art along with the current server version. + */ + public lolwut(options?: LolwutOptions): T { + return this.addAndReturn(createLolwut(options)); + } } /** diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index debe11285d..33fcaa61cf 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -297,6 +297,64 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "lolwut test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const result = await client.lolwut(); + expect(intoString(result)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result2 = await client.lolwut({ parameters: [] }); + expect(intoString(result2)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result3 = await client.lolwut({ parameters: [50, 20] }); + expect(intoString(result3)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result4 = await client.lolwut({ version: 6 }); + expect(intoString(result4)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result5 = await client.lolwut({ + version: 5, + parameters: [30, 4, 4], + }); + expect(intoString(result5)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // transaction tests + const transaction = new Transaction(); + transaction.lolwut(); + transaction.lolwut({ version: 5 }); + transaction.lolwut({ parameters: [1, 2] }); + transaction.lolwut({ version: 6, parameters: [42] }); + const results = await client.exec(transaction); + + if (results) { + for (const element of results) { + expect(intoString(element)).toEqual( + expect.stringContaining("Redis ver. "), + ); + } + } else { + throw new Error("Invalid LOLWUT transaction test results."); + } + + client.close(); + }, + TIMEOUT, + ); + runBaseTests({ init: async (protocol, clientName?) => { const options = getClientConfigurationOption( diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 734b98e006..63206b3f7a 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -456,4 +456,62 @@ describe("GlideClusterClient", () => { client.close(); }, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lolwut test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + // test with multi-node route + const result1 = await client.lolwut({}, "allNodes"); + expect(intoString(result1)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result2 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "allNodes", + ); + expect(intoString(result2)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // test with single-node route + const result3 = await client.lolwut({}, "randomNode"); + expect(intoString(result3)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result4 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "randomNode", + ); + expect(intoString(result4)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // transaction tests + const transaction = new ClusterTransaction(); + transaction.lolwut(); + transaction.lolwut({ version: 5 }); + transaction.lolwut({ parameters: [1, 2] }); + transaction.lolwut({ version: 6, parameters: [42] }); + const results = await client.exec(transaction); + + if (results) { + for (const element of results) { + expect(intoString(element)).toEqual( + expect.stringContaining("Redis ver. "), + ); + } + } else { + throw new Error("Invalid LOLWUT transaction test results."); + } + + client.close(); + }, + TIMEOUT, + ); }); From 4acccda2552beba5d445cf5fe0683b1e18477ff1 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:15:11 -0700 Subject: [PATCH 20/26] Node: add SMISMEMBER command (#1955) * Node: add SMISMEMBER command Signed-off-by: aaron-congo Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 23 +++++++++++++++++++++++ node/src/Commands.ts | 10 ++++++++++ node/src/Transaction.ts | 17 +++++++++++++++++ node/tests/SharedTests.ts | 37 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 6 ++++++ 6 files changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f92aaa5b2..301b91fb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Node: Added SINTERCARD command ([#1956](https://github.com/valkey-io/valkey-glide/pull/1956)) * Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) * Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) +* Node: Added SMISMEMBER command ([#1955](https://github.com/valkey-io/valkey-glide/pull/1955)) * Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924)) * Node: Added LOLWUT command ([#1934](https://github.com/valkey-io/valkey-glide/pull/1934)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 2173fab4d0..64774d5f73 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -84,6 +84,7 @@ import { createSInterStore, createSIsMember, createSMembers, + createSMIsMember, createSMove, createSPop, createSRem, @@ -1573,6 +1574,28 @@ export class BaseClient { return this.createWritePromise(createSIsMember(key, member)); } + /** + * Checks whether each member is contained in the members of the set stored at `key`. + * + * See https://valkey.io/commands/smismember/ for more details. + * + * @param key - The key of the set to check. + * @param members - A list of members to check for existence in the set. + * @returns An `array` of `boolean` values, each indicating if the respective member exists in the set. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.sadd("set1", ["a", "b", "c"]); + * const result = await client.smismember("set1", ["b", "c", "d"]); + * console.log(result); // Output: [true, true, false] - "b" and "c" are members of "set1", but "d" is not. + * ``` + */ + public smismember(key: string, members: string[]): Promise { + return this.createWritePromise(createSMIsMember(key, members)); + } + /** Removes and returns one random member from the set value store at `key`. * See https://valkey.io/commands/spop/ for details. * To pop multiple members, see `spopCount`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0509a6f741..2976c4ffd7 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -723,6 +723,16 @@ export function createSIsMember( return createCommand(RequestType.SIsMember, [key, member]); } +/** + * @internal + */ +export function createSMIsMember( + key: string, + members: string[], +): command_request.Command { + return createCommand(RequestType.SMIsMember, [key].concat(members)); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 890c8fe9a7..79fe2c55d4 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -89,6 +89,7 @@ import { createSInterStore, createSIsMember, createSMembers, + createSMIsMember, createSMove, createSPop, createSRem, @@ -903,6 +904,22 @@ export class BaseTransaction> { return this.addAndReturn(createSIsMember(key, member)); } + /** + * Checks whether each member is contained in the members of the set stored at `key`. + * + * See https://valkey.io/commands/smismember/ for more details. + * + * @param key - The key of the set to check. + * @param members - A list of members to check for existence in the set. + * + * Command Response - An `array` of `boolean` values, each indicating if the respective member exists in the set. + * + * since Valkey version 6.2.0. + */ + public smismember(key: string, members: string[]): T { + return this.addAndReturn(createSMIsMember(key, members)); + } + /** Removes and returns one random member from the set value store at `key`. * See https://valkey.io/commands/spop/ for details. * To pop multiple members, see `spopCount`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d99ec53051..ff8563eab8 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1677,6 +1677,43 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `smismember test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key = uuidv4(); + const stringKey = uuidv4(); + const nonExistingKey = uuidv4(); + + expect(await client.sadd(key, ["a", "b"])).toEqual(2); + expect(await client.smismember(key, ["b", "c"])).toEqual([ + true, + false, + ]); + + expect(await client.smismember(nonExistingKey, ["b"])).toEqual([ + false, + ]); + + // invalid argument - member list must not be empty + await expect(client.smismember(key, [])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.smismember(stringKey, ["a"]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `spop and spopCount test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d3c9c198a9..cd4cdb9f46 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -427,6 +427,12 @@ export async function transactionTest( args.push(1); baseTransaction.sismember(key7, "bar"); args.push(true); + + if (!(await checkIfServerVersionLessThan("6.2.0"))) { + baseTransaction.smismember(key7, ["bar", "foo", "baz"]); + args.push([true, true, false]); + } + baseTransaction.smembers(key7); args.push(new Set(["bar"])); baseTransaction.spop(key7); From 18eb4ad9ae996d7e0ce9711e5672946c6e79d5aa Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:19:13 -0700 Subject: [PATCH 21/26] Node: Add command FLUSHALL (#1958) Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/Commands.ts | 29 ++++++++++++++++ node/src/GlideClient.ts | 25 ++++++++++++++ node/src/GlideClusterClient.ts | 25 ++++++++++++++ node/src/Transaction.ts | 14 ++++++++ node/tests/RedisClusterClient.test.ts | 3 +- node/tests/SharedTests.ts | 48 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 8 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 301b91fb18..9be5cf8c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ * Python: Added FUNCTION STATS command ([#1794](https://github.com/valkey-io/valkey-glide/pull/1794)) * Python: Added XINFO STREAM command ([#1816](https://github.com/valkey-io/valkey-glide/pull/1816)) * Python: Added transaction supports for DUMP, RESTORE, FUNCTION DUMP and FUNCTION RESTORE ([#1814](https://github.com/valkey-io/valkey-glide/pull/1814)) +* Node: Added FlushAll command ([#1958](https://github.com/valkey-io/valkey-glide/pull/1958)) #### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/valkey-io/valkey-glide/pull/1494)) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 2976c4ffd7..f2579746b0 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1656,3 +1656,32 @@ export function createLolwut(options?: LolwutOptions): command_request.Command { return createCommand(RequestType.Lolwut, args); } + +/** + * Defines flushing mode for: + * + * `FLUSHALL` command. + * + * See https://valkey.io/commands/flushall/ for details. + */ +export enum FlushMode { + /** + * Flushes synchronously. + * + * since Valkey 6.2 and above. + */ + SYNC = "SYNC", + /** Flushes asynchronously. */ + ASYNC = "ASYNC", +} + +/** + * @internal + */ +export function createFlushAll(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FlushAll, [mode.toString()]); + } else { + return createCommand(RequestType.FlushAll, []); + } +} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index b7e76a4ba8..6a8ecd0322 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -5,6 +5,7 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { + FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -15,6 +16,7 @@ import { createConfigSet, createCustomCommand, createEcho, + createFlushAll, createInfo, createLolwut, createPing, @@ -330,4 +332,27 @@ export class GlideClient extends BaseClient { public lolwut(options?: LolwutOptions): Promise { return this.createWritePromise(createLolwut(options)); } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * The command will be routed to all primary nodes. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns `OK`. + * + * @example + * ```typescript + * const result = await client.flushall(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushall(mode?: FlushMode): Promise { + if (mode) { + return this.createWritePromise(createFlushAll(mode)); + } else { + return this.createWritePromise(createFlushAll()); + } + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 663925aae7..71856068da 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -5,6 +5,7 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { + FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -15,6 +16,7 @@ import { createConfigSet, createCustomCommand, createEcho, + createFlushAll, createInfo, createLolwut, createPing, @@ -595,4 +597,27 @@ export class GlideClusterClient extends BaseClient { toProtobufRoute(route), ); } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @param route - The command will be routed to all primaries, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns `OK`. + * + * @example + * ```typescript + * const result = await client.flushall(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushall(mode?: FlushMode, route?: Routes): Promise { + return this.createWritePromise( + createFlushAll(mode), + toProtobufRoute(route), + ); + } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 79fe2c55d4..48f6e5eed2 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -5,6 +5,7 @@ import { AggregationType, ExpireOptions, + FlushMode, InfoOptions, InsertPosition, KeyWeight, @@ -34,6 +35,7 @@ import { createExists, createExpire, createExpireAt, + createFlushAll, createGet, createGetDel, createHDel, @@ -1695,6 +1697,18 @@ export class BaseTransaction> { public lolwut(options?: LolwutOptions): T { return this.addAndReturn(createLolwut(options)); } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * Command Response - `OK`. + */ + public flushall(mode?: FlushMode): T { + return this.addAndReturn(createFlushAll(mode)); + } } /** diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 63206b3f7a..de89289814 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -47,7 +47,8 @@ describe("GlideClusterClient", () => { ? RedisCluster.initFromExistingCluster( parseEndpoints(clusterAddresses), ) - : await RedisCluster.createCluster(true, 3, 0); + : // setting replicaCount to 1 to facilitate tests routed to replicas + await RedisCluster.createCluster(true, 3, 1); }, 20000); afterEach(async () => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ff8563eab8..82f38ec2f8 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid"; import { ClosingError, ExpireOptions, + FlushMode, GlideClient, GlideClusterClient, InfoOptions, @@ -26,6 +27,7 @@ import { intoArray, intoString, } from "./TestUtilities"; +import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { @@ -3835,6 +3837,52 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `flushall test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + // Test FLUSHALL SYNC + expect(await client.flushall(FlushMode.SYNC)).toBe("OK"); + + // TODO: replace with KEYS command when implemented + const keysAfter = (await client.customCommand([ + "keys", + "*", + ])) as string[]; + expect(keysAfter.length).toBe(0); + + // Test various FLUSHALL calls + expect(await client.flushall()).toBe("OK"); + expect(await client.flushall(FlushMode.ASYNC)).toBe("OK"); + + if (client instanceof GlideClusterClient) { + const key = uuidv4(); + const primaryRoute: SingleNodeRoute = { + type: "primarySlotKey", + key: key, + }; + expect(await client.flushall(undefined, primaryRoute)).toBe( + "OK", + ); + expect( + await client.flushall(FlushMode.ASYNC, primaryRoute), + ).toBe("OK"); + + //Test FLUSHALL on replica (should fail) + const key2 = uuidv4(); + const replicaRoute: SingleNodeRoute = { + type: "replicaSlotKey", + key: key2, + }; + await expect( + client.flushall(undefined, replicaRoute), + ).rejects.toThrowError(); + } + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index cd4cdb9f46..707b8a3763 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -313,6 +313,8 @@ export async function transactionTest( const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; + baseTransaction.flushall(); + args.push("OK"); baseTransaction.set(key1, "bar"); args.push("OK"); baseTransaction.getdel(key1); From 10a2c93838b7d004b43e4fadee400af64da5b407 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Wed, 17 Jul 2024 17:07:28 -0700 Subject: [PATCH 22/26] Node: add `LPOS` command (#1927) * Added LPOS node command --------- Signed-off-by: Guian Gumpac Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 32 +++++++ node/src/Commands.ts | 18 ++++ node/src/Transaction.ts | 22 +++++ node/src/command-options/LPosOptions.ts | 64 ++++++++++++++ node/tests/SharedTests.ts | 109 ++++++++++++++++++++++++ node/tests/TestUtilities.ts | 18 ++++ 8 files changed, 266 insertions(+) create mode 100644 node/src/command-options/LPosOptions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be5cf8c7c..6a5ef5ed71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Node: Added SMISMEMBER command ([#1955](https://github.com/valkey-io/valkey-glide/pull/1955)) * Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924)) * Node: Added LOLWUT command ([#1934](https://github.com/valkey-io/valkey-glide/pull/1934)) +* Node: Added LPOS command ([#1927](https://github.com/valkey-io/valkey-glide/pull/1927)) ## 1.0.0 (2024-07-09) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index cfabd89a03..a560aa0823 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -85,6 +85,7 @@ function initialize() { PeriodicChecksManualInterval, PeriodicChecks, Logger, + LPosOptions, ExpireOptions, InfoOptions, InsertPosition, @@ -128,6 +129,7 @@ function initialize() { PeriodicChecksManualInterval, PeriodicChecks, Logger, + LPosOptions, ExpireOptions, InfoOptions, InsertPosition, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 64774d5f73..e31699eed3 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -10,6 +10,7 @@ import { } from "glide-rs"; import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; +import { LPosOptions } from "./command-options/LPosOptions"; import { AggregationType, ExpireOptions, @@ -52,6 +53,7 @@ import { createLInsert, createLLen, createLPop, + createLPos, createLPush, createLPushX, createLRange, @@ -2866,6 +2868,36 @@ export class BaseClient { return this.createWritePromise(createObjectRefcount(key)); } + /** + * Returns the index of the first occurrence of `element` inside the list specified by `key`. If no + * match is found, `null` is returned. If the `count` option is specified, then the function returns + * an `array` of indices of matching elements within the list. + * + * See https://valkey.io/commands/lpos/ for more details. + * + * @param key - The name of the list. + * @param element - The value to search for within the list. + * @param options - The LPOS options. + * @returns The index of `element`, or `null` if `element` is not in the list. If the `count` option + * is specified, then the function returns an `array` of indices of matching elements within the list. + * + * since - Valkey version 6.0.6. + * + * @example + * ```typescript + * await client.rpush("myList", ["a", "b", "c", "d", "e", "e"]); + * console.log(await client.lpos("myList", "e", new LPosOptions({ rank: 2 }))); // Output: 5 - the second occurrence of "e" is at index 5. + * console.log(await client.lpos("myList", "e", new LPosOptions({ count: 3 }))); // Output: [ 4, 5 ] - indices for the occurrences of "e" in list "myList". + * ``` + */ + public lpos( + key: string, + element: string, + options?: LPosOptions, + ): Promise { + return this.createWritePromise(createLPos(key, element, options)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f2579746b0..af75d23d2b 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -4,6 +4,7 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; +import { LPosOptions } from "./command-options/LPosOptions"; import { command_request } from "./ProtobufMessage"; @@ -1685,3 +1686,20 @@ export function createFlushAll(mode?: FlushMode): command_request.Command { return createCommand(RequestType.FlushAll, []); } } + +/** + * @internal + */ +export function createLPos( + key: string, + element: string, + options?: LPosOptions, +): command_request.Command { + let args: string[] = [key, element]; + + if (options) { + args = args.concat(options.toArgs()); + } + + return createCommand(RequestType.LPos, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 48f6e5eed2..d94c37ec62 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,6 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { LPosOptions } from "./command-options/LPosOptions"; import { AggregationType, ExpireOptions, @@ -57,6 +58,7 @@ import { createLInsert, createLLen, createLPop, + createLPos, createLPush, createLPushX, createLRange, @@ -1709,6 +1711,26 @@ export class BaseTransaction> { public flushall(mode?: FlushMode): T { return this.addAndReturn(createFlushAll(mode)); } + + /** + * Returns the index of the first occurrence of `element` inside the list specified by `key`. If no + * match is found, `null` is returned. If the `count` option is specified, then the function returns + * an `array` of indices of matching elements within the list. + * + * See https://valkey.io/commands/lpos/ for more details. + * + * @param key - The name of the list. + * @param element - The value to search for within the list. + * @param options - The LPOS options. + * + * Command Response - The index of `element`, or `null` if `element` is not in the list. If the `count` + * option is specified, then the function returns an `array` of indices of matching elements within the list. + * + * since - Valkey version 6.0.6. + */ + public lpos(key: string, element: string, options?: LPosOptions): T { + return this.addAndReturn(createLPos(key, element, options)); + } } /** diff --git a/node/src/command-options/LPosOptions.ts b/node/src/command-options/LPosOptions.ts new file mode 100644 index 0000000000..de2c0bcc2a --- /dev/null +++ b/node/src/command-options/LPosOptions.ts @@ -0,0 +1,64 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +/** + * Optional arguments to LPOS command. + * + * See https://valkey.io/commands/lpos/ for more details. + */ +export class LPosOptions { + /** Redis API keyword use to determine the rank of the match to return. */ + public static RANK_REDIS_API = "RANK"; + /** Redis API keyword used to extract specific number of matching indices from a list. */ + public static COUNT_REDIS_API = "COUNT"; + /** Redis API keyword used to determine the maximum number of list items to compare. */ + public static MAXLEN_REDIS_API = "MAXLEN"; + /** The rank of the match to return. */ + private rank?: number; + /** The specific number of matching indices from a list. */ + private count?: number; + /** The maximum number of comparisons to make between the element and the items in the list. */ + private maxLength?: number; + + constructor({ + rank, + count, + maxLength, + }: { + rank?: number; + count?: number; + maxLength?: number; + }) { + this.rank = rank; + this.count = count; + this.maxLength = maxLength; + } + + /** + * + * Converts LPosOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args: string[] = []; + + if (this.rank !== undefined) { + args.push(LPosOptions.RANK_REDIS_API); + args.push(this.rank.toString()); + } + + if (this.count !== undefined) { + args.push(LPosOptions.COUNT_REDIS_API); + args.push(this.count.toString()); + } + + if (this.maxLength !== undefined) { + args.push(LPosOptions.MAXLEN_REDIS_API); + args.push(this.maxLength.toString()); + } + + return args; + } +} diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 82f38ec2f8..048efa6fc0 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -28,6 +28,7 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; +import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { @@ -3883,6 +3884,114 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lpos test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}:${uuidv4()}`; + const valueArray = ["a", "a", "b", "c", "a", "b"]; + expect(await client.rpush(key, valueArray)).toEqual(6); + + // simplest case + expect(await client.lpos(key, "a")).toEqual(0); + expect( + await client.lpos(key, "b", new LPosOptions({ rank: 2 })), + ).toEqual(5); + + // element doesn't exist + expect(await client.lpos(key, "e")).toBeNull(); + + // reverse traversal + expect( + await client.lpos(key, "b", new LPosOptions({ rank: -2 })), + ).toEqual(2); + + // unlimited comparisons + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 1, maxLength: 0 }), + ), + ).toEqual(0); + + // limited comparisons + expect( + await client.lpos( + key, + "c", + new LPosOptions({ rank: 1, maxLength: 2 }), + ), + ).toBeNull(); + + // invalid rank value + await expect( + client.lpos(key, "a", new LPosOptions({ rank: 0 })), + ).rejects.toThrow(RequestError); + + // invalid maxlen value + await expect( + client.lpos(key, "a", new LPosOptions({ maxLength: -1 })), + ).rejects.toThrow(RequestError); + + // non-existent key + expect(await client.lpos("non-existent_key", "e")).toBeNull(); + + // wrong key data type + const wrongDataType = `{key}:${uuidv4()}`; + expect(await client.sadd(wrongDataType, ["a", "b"])).toEqual(2); + + await expect(client.lpos(wrongDataType, "a")).rejects.toThrow( + RequestError, + ); + + // invalid count value + await expect( + client.lpos(key, "a", new LPosOptions({ count: -1 })), + ).rejects.toThrow(RequestError); + + // with count + expect( + await client.lpos(key, "a", new LPosOptions({ count: 2 })), + ).toEqual([0, 1]); + expect( + await client.lpos(key, "a", new LPosOptions({ count: 0 })), + ).toEqual([0, 1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 1, count: 0 }), + ), + ).toEqual([0, 1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 2, count: 0 }), + ), + ).toEqual([1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 3, count: 0 }), + ), + ).toEqual([4]); + + // reverse traversal + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: -1, count: 0 }), + ), + ).toEqual([4, 1, 0]); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 707b8a3763..fc550f39af 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -19,6 +19,7 @@ import { Transaction, } from ".."; import { checkIfServerVersionLessThan } from "./SharedTests"; +import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; beforeAll(() => { Logger.init("info"); @@ -310,6 +311,7 @@ export async function transactionTest( const key13 = "{key}" + uuidv4(); const key14 = "{key}" + uuidv4(); // sorted set const key15 = "{key}" + uuidv4(); // list + const key16 = "{key}" + uuidv4(); // list const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -401,6 +403,22 @@ export async function transactionTest( args.push(0); baseTransaction.lpushx(key15, ["_"]); args.push(0); + baseTransaction.rpush(key16, [ + field + "1", + field + "1", + field + "2", + field + "3", + field + "3", + ]); + args.push(5); + baseTransaction.lpos(key16, field + "1", new LPosOptions({ rank: 2 })); + args.push(1); + baseTransaction.lpos( + key16, + field + "1", + new LPosOptions({ rank: 2, count: 0 }), + ); + args.push([1]); baseTransaction.sadd(key7, ["bar", "foo"]); args.push(2); baseTransaction.sunionstore(key7, [key7, key7]); From af9f26facf2a3479742de1c429f0d3586bb26d9f Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:34:31 -0700 Subject: [PATCH 23/26] Node: Add command DBSize (#1932) Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/Commands.ts | 7 +++++ node/src/GlideClient.ts | 18 +++++++++++++ node/src/GlideClusterClient.ts | 21 +++++++++++++++ node/src/Transaction.ts | 12 +++++++++ node/tests/SharedTests.ts | 48 ++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 7 files changed, 109 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5ef5ed71..b64ec945eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ * Python: Added XINFO STREAM command ([#1816](https://github.com/valkey-io/valkey-glide/pull/1816)) * Python: Added transaction supports for DUMP, RESTORE, FUNCTION DUMP and FUNCTION RESTORE ([#1814](https://github.com/valkey-io/valkey-glide/pull/1814)) * Node: Added FlushAll command ([#1958](https://github.com/valkey-io/valkey-glide/pull/1958)) +* Node: Added DBSize command ([#1932](https://github.com/valkey-io/valkey-glide/pull/1932)) #### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/valkey-io/valkey-glide/pull/1494)) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index af75d23d2b..ab6b32ada6 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1703,3 +1703,10 @@ export function createLPos( return createCommand(RequestType.LPos, args); } + +/** + * @internal + */ +export function createDBSize(): command_request.Command { + return createCommand(RequestType.DBSize, []); +} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 6a8ecd0322..8e4c86fc73 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -15,6 +15,7 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createEcho, createFlushAll, createInfo, @@ -355,4 +356,21 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFlushAll()); } } + + /** + * Returns the number of keys in the currently selected database. + * + * See https://valkey.io/commands/dbsize/ for more details. + * + * @returns The number of keys in the currently selected database. + * + * @example + * ```typescript + * const numKeys = await client.dbsize(); + * console.log("Number of keys in the current database: ", numKeys); + * ``` + */ + public dbsize(): Promise { + return this.createWritePromise(createDBSize()); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 71856068da..6d01fb90ee 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -15,6 +15,7 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createEcho, createFlushAll, createInfo, @@ -620,4 +621,24 @@ export class GlideClusterClient extends BaseClient { toProtobufRoute(route), ); } + + /** + * Returns the number of keys in the database. + * + * See https://valkey.io/commands/dbsize/ for more details. + + * @param route - The command will be routed to all primaries, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns The number of keys in the database. + * In the case of routing the query to multiple nodes, returns the aggregated number of keys across the different nodes. + * + * @example + * ```typescript + * const numKeys = await client.dbsize("allPrimaries"); + * console.log("Number of keys across all primary nodes: ", numKeys); + * ``` + */ + public dbsize(route?: Routes): Promise> { + return this.createWritePromise(createDBSize(), toProtobufRoute(route)); + } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index d94c37ec62..7beb57a922 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -29,6 +29,7 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createDecr, createDecrBy, createDel, @@ -1731,6 +1732,17 @@ export class BaseTransaction> { public lpos(key: string, element: string, options?: LPosOptions): T { return this.addAndReturn(createLPos(key, element, options)); } + + /** + * Returns the number of keys in the currently selected database. + * + * See https://valkey.io/commands/dbsize/ for more details. + * + * Command Response - The number of keys in the currently selected database. + */ + public dbsize(): T { + return this.addAndReturn(createDBSize()); + } } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 048efa6fc0..248c437fef 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3992,6 +3992,54 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `dbsize test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + // flush all data + expect(await client.flushall()).toBe("OK"); + + // check that DBSize is 0 + expect(await client.dbsize()).toBe(0); + + // set 10 random key-value pairs + for (let i = 0; i < 10; i++) { + const key = `{key}:${uuidv4()}`; + const value = "0".repeat(Math.random() * 7); + + expect(await client.set(key, value)).toBe("OK"); + } + + // check DBSIZE after setting + expect(await client.dbsize()).toBe(10); + + // additional test for the standalone client + if (client instanceof GlideClient) { + expect(await client.flushall()).toBe("OK"); + const key = uuidv4(); + expect(await client.set(key, "value")).toBe("OK"); + expect(await client.dbsize()).toBe(1); + // switching to another db to check size + expect(await client.select(1)).toBe("OK"); + expect(await client.dbsize()).toBe(0); + } + + // additional test for the cluster client + if (client instanceof GlideClusterClient) { + expect(await client.flushall()).toBe("OK"); + const key = uuidv4(); + expect(await client.set(key, "value")).toBe("OK"); + const primaryRoute: SingleNodeRoute = { + type: "primarySlotKey", + key: key, + }; + expect(await client.dbsize(primaryRoute)).toBe(1); + } + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fc550f39af..e58f27e79e 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -317,6 +317,8 @@ export async function transactionTest( const args: ReturnType[] = []; baseTransaction.flushall(); args.push("OK"); + baseTransaction.dbsize(); + args.push(0); baseTransaction.set(key1, "bar"); args.push("OK"); baseTransaction.getdel(key1); From 4a48337093921e1bc4613084db8217e6dd1cfcc4 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:44:19 +0300 Subject: [PATCH 24/26] Node: add PubSub support (#1964) Signed-off-by: Shoham Elias Signed-off-by: Chloe Yip --- node/npm/glide/index.ts | 4 + node/src/BaseClient.ts | 313 +++++++++++++++++++++++--- node/src/Commands.ts | 12 + node/src/Errors.ts | 3 + node/src/GlideClient.ts | 74 +++++- node/src/GlideClusterClient.ts | 93 +++++++- node/tests/RedisClient.test.ts | 85 ++++++- node/tests/RedisClusterClient.test.ts | 57 +++++ 8 files changed, 610 insertions(+), 31 deletions(-) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index a560aa0823..cc181ad00e 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -101,6 +101,7 @@ function initialize() { StreamReadOptions, ScriptOptions, ClosingError, + ConfigurationError, ExecAbortError, RedisError, RequestError, @@ -108,6 +109,7 @@ function initialize() { ConnectionError, ClusterTransaction, Transaction, + PubSubMsg, createLeakedArray, createLeakedAttribute, createLeakedBigint, @@ -145,6 +147,7 @@ function initialize() { StreamReadOptions, ScriptOptions, ClosingError, + ConfigurationError, ExecAbortError, RedisError, RequestError, @@ -152,6 +155,7 @@ function initialize() { ConnectionError, ClusterTransaction, Transaction, + PubSubMsg, createLeakedArray, createLeakedAttribute, createLeakedBigint, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e31699eed3..3915c59a19 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -85,8 +85,8 @@ import { createSInterCard, createSInterStore, createSIsMember, - createSMembers, createSMIsMember, + createSMembers, createSMove, createSPop, createSRem, @@ -118,12 +118,15 @@ import { } from "./Commands"; import { ClosingError, + ConfigurationError, ConnectionError, ExecAbortError, RedisError, RequestError, TimeoutError, } from "./Errors"; +import { GlideClientConfiguration } from "./GlideClient"; +import { ClusterClientConfiguration } from "./GlideClusterClient"; import { Logger } from "./Logger"; import { command_request, @@ -267,6 +270,11 @@ function getRequestErrorClass( return RequestError; } +export type PubSubMsg = { + message: string; + channel: string; + pattern?: string | null; +}; export class BaseClient { private socket: net.Socket; private readonly promiseCallbackFunctions: [ @@ -279,7 +287,54 @@ export class BaseClient { private remainingReadData: Uint8Array | undefined; private readonly requestTimeout: number; // Timeout in milliseconds private isClosed = false; + private readonly pubsubFutures: [PromiseFunction, ErrorFunction][] = []; + private pendingPushNotification: response.Response[] = []; + private config: BaseClientConfiguration | undefined; + + protected configurePubsub( + options: ClusterClientConfiguration | GlideClientConfiguration, + configuration: connection_request.IConnectionRequest, + ) { + if (options.pubsubSubscriptions) { + if (options.protocol == ProtocolVersion.RESP2) { + throw new ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured.", + ); + } + const { context, callback } = options.pubsubSubscriptions; + + if (context && !callback) { + throw new ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured.", + ); + } + + configuration.pubsubSubscriptions = + connection_request.PubSubSubscriptions.create({}); + + for (const [channelType, channelsPatterns] of Object.entries( + options.pubsubSubscriptions.channelsAndPatterns, + )) { + let entry = + configuration.pubsubSubscriptions! + .channelsOrPatternsByType![parseInt(channelType)]; + + if (!entry) { + entry = connection_request.PubSubChannelsOrPatterns.create({ + channelsOrPatterns: [], + }); + configuration.pubsubSubscriptions!.channelsOrPatternsByType![ + parseInt(channelType) + ] = entry; + } + + for (const channelPattern of channelsPatterns) { + entry.channelsOrPatterns!.push(Buffer.from(channelPattern)); + } + } + } + } private handleReadData(data: Buffer) { const buf = this.remainingReadData ? Buffer.concat([this.remainingReadData, data]) @@ -307,40 +362,69 @@ export class BaseClient { } } - if (message.closingError != null) { - this.close(message.closingError); - return; + if (message.isPush) { + this.processPush(message); + } else { + this.processResponse(message); } + } - const [resolve, reject] = - this.promiseCallbackFunctions[message.callbackIdx]; - this.availableCallbackSlots.push(message.callbackIdx); + this.remainingReadData = undefined; + } - if (message.requestError != null) { - const errorType = getRequestErrorClass( - message.requestError.type, - ); - reject( - new errorType(message.requestError.message ?? undefined), - ); - } else if (message.respPointer != null) { - const pointer = message.respPointer; + processResponse(message: response.Response) { + if (message.closingError != null) { + this.close(message.closingError); + return; + } - if (typeof pointer === "number") { - resolve(valueFromSplitPointer(0, pointer)); - } else { - resolve(valueFromSplitPointer(pointer.high, pointer.low)); - } - } else if ( - message.constantResponse === response.ConstantResponse.OK - ) { - resolve("OK"); + const [resolve, reject] = + this.promiseCallbackFunctions[message.callbackIdx]; + this.availableCallbackSlots.push(message.callbackIdx); + + if (message.requestError != null) { + const errorType = getRequestErrorClass(message.requestError.type); + reject(new errorType(message.requestError.message ?? undefined)); + } else if (message.respPointer != null) { + const pointer = message.respPointer; + + if (typeof pointer === "number") { + resolve(valueFromSplitPointer(0, pointer)); } else { - resolve(null); + resolve(valueFromSplitPointer(pointer.high, pointer.low)); } + } else if (message.constantResponse === response.ConstantResponse.OK) { + resolve("OK"); + } else { + resolve(null); } + } - this.remainingReadData = undefined; + processPush(response: response.Response) { + if (response.closingError != null || !response.respPointer) { + const errMsg = response.closingError + ? response.closingError + : "Client Error - push notification without resp_pointer"; + + this.close(errMsg); + return; + } + + const [callback, context] = this.getPubsubCallbackAndContext( + this.config!, + ); + + if (callback) { + const pubsubMessage = + this.notificationToPubSubMessageSafe(response); + + if (pubsubMessage) { + callback(pubsubMessage, context); + } + } else { + this.pendingPushNotification.push(response); + this.completePubSubFuturesSafe(); + } } /** @@ -352,6 +436,7 @@ export class BaseClient { ) { // if logger has been initialized by the external-user on info level this log will be shown Logger.log("info", "Client lifetime", `construct client`); + this.config = options; this.requestTimeout = options?.requestTimeout ?? DEFAULT_TIMEOUT_IN_MILLISECONDS; this.socket = socket; @@ -473,6 +558,175 @@ export class BaseClient { return result; } + cancelPubSubFuturesWithExceptionSafe(exception: ConnectionError): void { + while (this.pubsubFutures.length > 0) { + const nextFuture = this.pubsubFutures.shift(); + + if (nextFuture) { + const [, reject] = nextFuture; + reject(exception); + } + } + } + + isPubsubConfigured( + config: GlideClientConfiguration | ClusterClientConfiguration, + ): boolean { + return !!config.pubsubSubscriptions; + } + + getPubsubCallbackAndContext( + config: GlideClientConfiguration | ClusterClientConfiguration, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ): [((msg: PubSubMsg, context: any) => void) | null | undefined, any] { + if (config.pubsubSubscriptions) { + return [ + config.pubsubSubscriptions.callback, + config.pubsubSubscriptions.context, + ]; + } + + return [null, null]; + } + + public getPubSubMessage(): Promise { + if (this.isClosed) { + throw new ClosingError( + "Unable to execute requests; the client is closed. Please create a new client.", + ); + } + + if (!this.isPubsubConfigured(this.config!)) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsbub subscriptions applied to the client.", + ); + } + + if (this.getPubsubCallbackAndContext(this.config!)[0]) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured callback.", + ); + } + + return new Promise((resolve, reject) => { + this.pubsubFutures.push([resolve, reject]); + this.completePubSubFuturesSafe(); + }); + } + + public tryGetPubSubMessage(): PubSubMsg | null { + if (this.isClosed) { + throw new ClosingError( + "Unable to execute requests; the client is closed. Please create a new client.", + ); + } + + if (!this.isPubsubConfigured(this.config!)) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsbub subscriptions applied to the client.", + ); + } + + if (this.getPubsubCallbackAndContext(this.config!)[0]) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured callback.", + ); + } + + let msg: PubSubMsg | null = null; + this.completePubSubFuturesSafe(); + + while (this.pendingPushNotification.length > 0 && !msg) { + const pushNotification = this.pendingPushNotification.shift()!; + msg = this.notificationToPubSubMessageSafe(pushNotification); + } + + return msg; + } + notificationToPubSubMessageSafe( + pushNotification: response.Response, + ): PubSubMsg | null { + let msg: PubSubMsg | null = null; + const responsePointer = pushNotification.respPointer; + let nextPushNotificationValue: Record = {}; + + if (responsePointer) { + if (typeof responsePointer !== "number") { + nextPushNotificationValue = valueFromSplitPointer( + responsePointer.high, + responsePointer.low, + ) as Record; + } else { + nextPushNotificationValue = valueFromSplitPointer( + 0, + responsePointer, + ) as Record; + } + + const messageKind = nextPushNotificationValue["kind"]; + + if (messageKind === "Disconnect") { + Logger.log( + "warn", + "disconnect notification", + "Transport disconnected, messages might be lost", + ); + } else if ( + messageKind === "Message" || + messageKind === "PMessage" || + messageKind === "SMessage" + ) { + const values = nextPushNotificationValue["values"] as string[]; + + if (messageKind === "PMessage") { + msg = { + message: values[2], + channel: values[1], + pattern: values[0], + }; + } else { + msg = { + message: values[1], + channel: values[0], + pattern: null, + }; + } + } else if ( + messageKind === "PSubscribe" || + messageKind === "Subscribe" || + messageKind === "SSubscribe" || + messageKind === "Unsubscribe" || + messageKind === "SUnsubscribe" || + messageKind === "PUnsubscribe" + ) { + // pass + } else { + Logger.log( + "error", + "unknown notification", + `Unknown notification: '${messageKind}'`, + ); + } + } + + return msg; + } + completePubSubFuturesSafe() { + while ( + this.pendingPushNotification.length > 0 && + this.pubsubFutures.length > 0 + ) { + const nextPushNotification = this.pendingPushNotification.shift()!; + const pubsubMessage = + this.notificationToPubSubMessageSafe(nextPushNotification); + + if (pubsubMessage) { + const [resolve] = this.pubsubFutures.shift()!; + resolve(pubsubMessage); + } + } + } + /** Get the value associated with the given key, or null if no such value exists. * See https://valkey.io/commands/get/ for details. * @@ -2968,6 +3222,11 @@ export class BaseClient { this.promiseCallbackFunctions.forEach(([, reject]) => { reject(new ClosingError(errorMessage)); }); + + // Handle pubsub futures + this.pubsubFutures.forEach(([, reject]) => { + reject(new ClosingError(errorMessage || "")); + }); Logger.log("info", "Client lifetime", "disposing of client"); this.socket.end(); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ab6b32ada6..a3ddfccc26 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1473,6 +1473,18 @@ export function createTime(): command_request.Command { return createCommand(RequestType.Time, []); } +/** + * @internal + */ +export function createPublish( + message: string, + channel: string, + sharded: boolean = false, +): command_request.Command { + const request = sharded ? RequestType.SPublish : RequestType.Publish; + return createCommand(request, [channel, message]); +} + /** * @internal */ diff --git a/node/src/Errors.ts b/node/src/Errors.ts index d4a73f2958..8fa95fa6dd 100644 --- a/node/src/Errors.ts +++ b/node/src/Errors.ts @@ -32,3 +32,6 @@ export class ExecAbortError extends RequestError {} /// Errors that are thrown when a connection disconnects. These errors can be temporary, as the client will attempt to reconnect. export class ConnectionError extends RequestError {} + +/// Errors that are thrown when a request cannot be completed in current configuration settings. +export class ConfigurationError extends RequestError {} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 8e4c86fc73..a6700cd941 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -3,7 +3,12 @@ */ import * as net from "net"; -import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; +import { + BaseClient, + BaseClientConfiguration, + PubSubMsg, + ReturnType, +} from "./BaseClient"; import { FlushMode, InfoOptions, @@ -21,12 +26,51 @@ import { createInfo, createLolwut, createPing, + createPublish, createSelect, createTime, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +export namespace GlideClientConfiguration { + /** + * Enum representing pubsub subscription modes. + * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + */ + export enum PubSubChannelModes { + /** + * Use exact channel names. + */ + Exact = 0, + + /** + * Use channel name patterns. + */ + Pattern = 1, + } + + export type PubSubSubscriptions = { + /** + * Channels and patterns by modes. + */ + channelsAndPatterns: Partial>>; + + /** + * Optional callback to accept the incoming messages. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + callback?: (msg: PubSubMsg, context: any) => void; + + /** + * Arbitrary context to pass to the callback. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + context?: any; + }; +} + export type GlideClientConfiguration = BaseClientConfiguration & { /** * index of the logical database to connect to. @@ -57,6 +101,11 @@ export type GlideClientConfiguration = BaseClientConfiguration & { */ exponentBase: number; }; + /** + * PubSub subscriptions to be used for the client. + * Will be applied via SUBSCRIBE/PSUBSCRIBE commands during connection establishment. + */ + pubsubSubscriptions?: GlideClientConfiguration.PubSubSubscriptions; }; /** @@ -74,6 +123,7 @@ export class GlideClient extends BaseClient { const configuration = super.createClientRequest(options); configuration.databaseId = options.databaseId; configuration.connectionRetryStrategy = options.connectionBackoff; + this.configurePubsub(options, configuration); return configuration; } @@ -82,7 +132,8 @@ export class GlideClient extends BaseClient { ): Promise { return super.createClientInternal( options, - (socket: net.Socket) => new GlideClient(socket), + (socket: net.Socket, options?: GlideClientConfiguration) => + new GlideClient(socket, options), ); } @@ -373,4 +424,23 @@ export class GlideClient extends BaseClient { public dbsize(): Promise { return this.createWritePromise(createDBSize()); } + + /** Publish a message on pubsub channel. + * See https://valkey.io/commands/publish for more details. + * + * @param message - Message to publish. + * @param channel - Channel to publish the message on. + * @returns - Number of subscriptions in primary node that received the message. + * Note that this value does not include subscriptions that configured on replicas. + * + * @example + * ```typescript + * // Example usage of publish command + * const result = await client.publish("Hi all!", "global-channel"); + * console.log(result); // Output: 1 - This message was posted to 1 subscription which is configured on primary node + * ``` + */ + public publish(message: string, channel: string): Promise { + return this.createWritePromise(createPublish(message, channel)); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 6d01fb90ee..88a2baec1d 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -3,7 +3,12 @@ */ import * as net from "net"; -import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; +import { + BaseClient, + BaseClientConfiguration, + PubSubMsg, + ReturnType, +} from "./BaseClient"; import { FlushMode, InfoOptions, @@ -21,6 +26,7 @@ import { createInfo, createLolwut, createPing, + createPublish, createTime, } from "./Commands"; import { RequestError } from "./Errors"; @@ -53,6 +59,49 @@ export type PeriodicChecks = * Manually configured interval for periodic checks. */ | PeriodicChecksManualInterval; + +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +export namespace ClusterClientConfiguration { + /** + * Enum representing pubsub subscription modes. + * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + */ + export enum PubSubChannelModes { + /** + * Use exact channel names. + */ + Exact = 0, + + /** + * Use channel name patterns. + */ + Pattern = 1, + + /** + * Use sharded pubsub. Available since Valkey version 7.0. + */ + Sharded = 2, + } + + export type PubSubSubscriptions = { + /** + * Channels and patterns by modes. + */ + channelsAndPatterns: Partial>>; + + /** + * Optional callback to accept the incoming messages. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + callback?: (msg: PubSubMsg, context: any) => void; + + /** + * Arbitrary context to pass to the callback. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + context?: any; + }; +} export type ClusterClientConfiguration = BaseClientConfiguration & { /** * Configure the periodic topology checks. @@ -61,6 +110,12 @@ export type ClusterClientConfiguration = BaseClientConfiguration & { * If not set, `enabledDefaultConfigs` will be used. */ periodicChecks?: PeriodicChecks; + + /** + * PubSub subscriptions to be used for the client. + * Will be applied via SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE commands during connection establishment. + */ + pubsubSubscriptions?: ClusterClientConfiguration.PubSubSubscriptions; }; /** @@ -235,6 +290,7 @@ export class GlideClusterClient extends BaseClient { } } + this.configurePubsub(options, configuration); return configuration; } @@ -641,4 +697,39 @@ export class GlideClusterClient extends BaseClient { public dbsize(route?: Routes): Promise> { return this.createWritePromise(createDBSize(), toProtobufRoute(route)); } + + /** Publish a message on pubsub channel. + * This command aggregates PUBLISH and SPUBLISH commands functionalities. + * The mode is selected using the 'sharded' parameter. + * For both sharded and non-sharded mode, request is routed using hashed channel as key. + * See https://valkey.io/commands/publish and https://valkey.io/commands/spublish for more details. + * + * @param message - Message to publish. + * @param channel - Channel to publish the message on. + * @param sharded - Use sharded pubsub mode. Available since Valkey version 7.0. + * @returns - Number of subscriptions in primary node that received the message. + * + * @example + * ```typescript + * // Example usage of publish command + * const result = await client.publish("Hi all!", "global-channel"); + * console.log(result); // Output: 1 - This message was posted to 1 subscription which is configured on primary node + * ``` + * + * @example + * ```typescript + * // Example usage of spublish command + * const result = await client.publish("Hi all!", "global-channel", true); + * console.log(result); // Output: 2 - Published 2 instances of "Hi to sharded channel1!" message on channel1 using sharded mode + * ``` + */ + public publish( + message: string, + channel: string, + sharded: boolean = false, + ): Promise { + return this.createWritePromise( + createPublish(message, channel, sharded), + ); + } } diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 33fcaa61cf..16e2572dae 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -12,7 +12,13 @@ import { } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; -import { GlideClient, ProtocolVersion, Transaction } from ".."; +import { + GlideClient, + GlideClientConfiguration, + ProtocolVersion, + PubSubMsg, + Transaction, +} from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; @@ -355,6 +361,83 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP3])("simple pubsub test", async (protocol) => { + const pattern = "*"; + const channel = "test-channel"; + const config: GlideClientConfiguration = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const channelsAndPatterns: Partial< + Record> + > = { + [GlideClientConfiguration.PubSubChannelModes.Exact]: new Set([ + channel, + ]), + [GlideClientConfiguration.PubSubChannelModes.Pattern]: new Set([ + pattern, + ]), + }; + config.pubsubSubscriptions = { + channelsAndPatterns: channelsAndPatterns, + }; + client = await GlideClient.createClient(config); + const clientTry = await GlideClient.createClient(config); + const context: PubSubMsg[] = []; + + function new_message(msg: PubSubMsg, context: PubSubMsg[]): void { + context.push(msg); + } + + const clientCallback = await GlideClient.createClient({ + addresses: config.addresses, + pubsubSubscriptions: { + channelsAndPatterns: channelsAndPatterns, + callback: new_message, + context: context, + }, + }); + const message = uuidv4(); + const asyncMessages: PubSubMsg[] = []; + const tryMessages: (PubSubMsg | null)[] = []; + + await client.publish(message, "test-channel"); + const sleep = new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep; + + for (let i = 0; i < 2; i++) { + asyncMessages.push(await client.getPubSubMessage()); + tryMessages.push(clientTry.tryGetPubSubMessage()); + } + + expect(clientTry.tryGetPubSubMessage()).toBeNull(); + expect(asyncMessages.length).toBe(2); + expect(tryMessages.length).toBe(2); + expect(context.length).toBe(2); + + // assert all api flavors produced the same messages + expect(asyncMessages).toEqual(tryMessages); + expect(asyncMessages).toEqual(context); + + let patternCount = 0; + + for (let i = 0; i < 2; i++) { + const pubsubMsg = asyncMessages[i]; + expect(pubsubMsg.channel.toString()).toBe(channel); + expect(pubsubMsg.message.toString()).toBe(message); + + if (pubsubMsg.pattern) { + patternCount++; + expect(pubsubMsg.pattern.toString()).toBe(pattern); + } + } + + expect(patternCount).toBe(1); + client.close(); + clientTry.close(); + clientCallback.close(); + }); + runBaseTests({ init: async (protocol, clientName?) => { const options = getClientConfigurationOption( diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index de89289814..2b656aea45 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -13,6 +13,7 @@ import { import { v4 as uuidv4 } from "uuid"; import { + ClusterClientConfiguration, ClusterTransaction, GlideClusterClient, InfoOptions, @@ -515,4 +516,60 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + + it.each([ + [true, ProtocolVersion.RESP3], + [false, ProtocolVersion.RESP3], + ])("simple pubsub test", async (sharded, protocol) => { + if (sharded && (await checkIfServerVersionLessThan("7.2.0"))) { + return; + } + + const channel = "test-channel"; + const shardedChannel = "test-channel-sharded"; + const channelsAndPatterns: Partial< + Record> + > = { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: new Set([ + channel, + ]), + }; + + if (sharded) { + channelsAndPatterns[ + ClusterClientConfiguration.PubSubChannelModes.Sharded + ] = new Set([shardedChannel]); + } + + const config: ClusterClientConfiguration = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + config.pubsubSubscriptions = { + channelsAndPatterns: channelsAndPatterns, + }; + client = await GlideClusterClient.createClient(config); + const message = uuidv4(); + + await client.publish(message, channel); + const sleep = new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep; + + let pubsubMsg = await client.getPubSubMessage(); + expect(pubsubMsg.channel.toString()).toBe(channel); + expect(pubsubMsg.message.toString()).toBe(message); + expect(pubsubMsg.pattern).toBeNull(); + + if (sharded) { + await client.publish(message, shardedChannel, true); + await sleep; + pubsubMsg = await client.getPubSubMessage(); + console.log(pubsubMsg); + expect(pubsubMsg.channel.toString()).toBe(shardedChannel); + expect(pubsubMsg.message.toString()).toBe(message); + expect(pubsubMsg.pattern).toBeNull(); + } + + client.close(); + }); }); From fae352609dde6207f9ba7ad0885b12b35a01d7bf Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:05:45 -0700 Subject: [PATCH 25/26] remove dash Co-authored-by: Yury-Fridlyand Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 3915c59a19..b946972bc5 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -750,7 +750,7 @@ export class BaseClient { * See https://valkey.io/commands/getdel/ for details. * * @param key - The key to retrieve from the database. - * @returns - If `key` exists, returns the `value` of `key`. Otherwise, return `null`. + * @returns If `key` exists, returns the `value` of `key`. Otherwise, return `null`. * * @example * ```typescript From 01385836735e73a127ae6b5c3a9c4163caa8d21a Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 18 Jul 2024 11:10:27 -0700 Subject: [PATCH 26/26] ran linters and fixed changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b64ec945eb..657b747ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ #### Changes -* Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) * Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) * Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) * Node: Added SINTERCARD command ([#1956](https://github.com/valkey-io/valkey-glide/pull/1956))