From 637046de65a9e095dfa5e137f9889565bd20a107 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 21:53:22 -0700 Subject: [PATCH 001/236] 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 185265a5ca..9b20104b51 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -52,6 +52,7 @@ import { createLLen, createLPop, createLPush, + createLPushX, createLRange, createLRem, createLTrim, @@ -69,6 +70,7 @@ import { createPfCount, createRPop, createRPush, + createRPushX, createRename, createRenameNX, createSAdd, @@ -955,6 +957,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. @@ -1138,6 +1159,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 8d2f6608cd..c3b4fa395c 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -481,6 +481,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 */ @@ -550,6 +560,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 10cde919f0..598709830f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -54,6 +54,7 @@ import { createLLen, createLPop, createLPush, + createLPushX, createLRange, createLRem, createLTrim, @@ -72,6 +73,7 @@ import { createPing, createRPop, createRPush, + createRPushX, createRename, createRenameNX, createSAdd, @@ -528,6 +530,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. @@ -634,6 +650,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 7de522528c..93c39cde80 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -847,6 +847,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 d8200ffba84fadff926cbec2c0a1772def43b86c Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:26:37 -0700 Subject: [PATCH 002/236] 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 1d48feb832..b4f817c9d0 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -858,7 +858,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", @@ -866,11 +866,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 d9c4227389ae84076a4fba8b075acc1d766a37a9 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:30:36 -0700 Subject: [PATCH 003/236] 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 b4f817c9d0..f0a58261d5 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1075,6 +1075,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 915d68d06dd9939be72eebaf5f1ee3c74b358682 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:38:13 -0700 Subject: [PATCH 004/236] 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 5872a4bdee..54faa59f21 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[] = []; @@ -390,6 +391,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 4026536fdcc156c6355d34b9a6775651c058644c Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 16 Jul 2024 22:40:32 -0700 Subject: [PATCH 005/236] updated changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85848b0e6..26d1c09639 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 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)) From 087826b997e5c9b6aa6fb832d4e9c62ba51a90db Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 17 Jul 2024 10:05:44 -0700 Subject: [PATCH 006/236] Fixes for java client release pipeline (#1922) Signed-off-by: Yury-Fridlyand --- .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 6cc0343beed06eb3c2b446848f8fd39ad57b106d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 11:09:40 -0700 Subject: [PATCH 007/236] 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 4e9428c8d8..d93f643f5c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -966,7 +966,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"]); @@ -1187,11 +1187,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 c3d7a275a6..2a8615419a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -532,14 +532,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)); @@ -672,11 +673,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 54faa59f21..42d0e720f8 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[] = []; @@ -391,7 +391,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 c7581063169810c60510ff549022f62350d2ad6b Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 17 Jul 2024 11:37:24 -0700 Subject: [PATCH 008/236] 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 d93f643f5c..c15d636124 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -959,7 +959,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 bf8125dcff8dbea0905bd3bb3d10232ea508ed12 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 009/236] Node: add SINTERCARD command (#1956) * Node: add SINTERCARD command Signed-off-by: aaron-congo --- 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 26d1c09639..1e1250e4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * 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)) * 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 c15d636124..d7b37d2d3b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -79,6 +79,7 @@ import { createSDiff, createSDiffStore, createSInter, + createSInterCard, createSInterStore, createSIsMember, createSMembers, @@ -1385,6 +1386,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 9673972a87..be95c32c94 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -645,6 +645,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 2a8615419a..a87fcc37a0 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -82,6 +82,7 @@ import { createSDiff, createSDiffStore, createSInter, + createSInterCard, createSInterStore, createSIsMember, createSMembers, @@ -787,6 +788,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 f0a58261d5..9bc9ac431f 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1316,6 +1316,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 42d0e720f8..fa62288d3c 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -403,6 +403,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 432932f05dc1ea178df0691af9020f4d09df17ee Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 17 Jul 2024 13:14:40 -0700 Subject: [PATCH 010/236] Node: Add LOLWUT command (#1934) * Node: Add LOLWUT command Signed-off-by: Yi-Pin Chen --- 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 1e1250e4a2..6f1ebfbf12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,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 be95c32c94..2f78e7abfb 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1607,3 +1607,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 a87fcc37a0..128c0c402e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -8,6 +8,7 @@ import { InfoOptions, InsertPosition, KeyWeight, + LolwutOptions, RangeByIndex, RangeByLex, RangeByScore, @@ -59,6 +60,7 @@ import { createLRem, createLSet, createLTrim, + createLolwut, createMGet, createMSet, createObjectEncoding, @@ -89,6 +91,7 @@ import { createSMove, createSPop, createSRem, + createSUnion, createSUnionStore, createSelect, createSet, @@ -115,7 +118,6 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, - createSUnion, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1649,6 +1651,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 6baf4af5898cf0a7fe2c4e449de329aef3821948 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 011/236] Node: add SMISMEMBER command (#1955) * Node: add SMISMEMBER command Signed-off-by: aaron-congo --- 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 6f1ebfbf12..aead2ace64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,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 d7b37d2d3b..34d4ead4fc 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -83,6 +83,7 @@ import { createSInterStore, createSIsMember, createSMembers, + createSMIsMember, createSMove, createSPop, createSRem, @@ -1552,6 +1553,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 2f78e7abfb..c68efc25ac 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -716,6 +716,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 128c0c402e..c72dee1686 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -88,6 +88,7 @@ import { createSInterStore, createSIsMember, createSMembers, + createSMIsMember, createSMove, createSPop, createSRem, @@ -889,6 +890,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 9bc9ac431f..a8740143a9 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1657,6 +1657,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 fa62288d3c..d9568a332f 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -423,6 +423,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 4ddb11d82a0c4a880da35ac3482f2f46358a8baf 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 012/236] Node: Add command FLUSHALL (#1958) --- 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 aead2ace64..ba9d10f21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,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 c68efc25ac..ae18cb159f 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1649,3 +1649,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 c72dee1686..eba56e8e71 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, createHDel, createHExists, @@ -1681,6 +1683,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 a8740143a9..5a99f723a5 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) => { @@ -3815,6 +3817,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 d9568a332f..b072516209 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.objectEncoding(key1); From ebb04c1f9923dc6f9435a5080bd0333b9a3b6909 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Wed, 17 Jul 2024 17:07:28 -0700 Subject: [PATCH 013/236] Node: add `LPOS` command (#1927) * Added LPOS node command --------- Signed-off-by: Guian Gumpac --- 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 ba9d10f21f..5eea78e1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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 34d4ead4fc..5faecf4c8a 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, @@ -51,6 +52,7 @@ import { createLInsert, createLLen, createLPop, + createLPos, createLPush, createLPushX, createLRange, @@ -2845,6 +2847,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 ae18cb159f..4b4e336511 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"; @@ -1678,3 +1679,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 eba56e8e71..9ce5720a3b 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, @@ -56,6 +57,7 @@ import { createLInsert, createLLen, createLPop, + createLPos, createLPush, createLPushX, createLRange, @@ -1695,6 +1697,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 5a99f723a5..00e6bf5741 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) => { @@ -3863,6 +3864,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 b072516209..7e0351886b 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[] = []; @@ -397,6 +399,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 d63ec24126461ee03342b0d4e933337af51d66f9 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 014/236] Node: Add command DBSize (#1932) --- 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 5eea78e1bf..a4e304e30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,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 4b4e336511..13d4baab17 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1696,3 +1696,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 9ce5720a3b..d2f637f77e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -29,6 +29,7 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createDecr, createDecrBy, createDel, @@ -1717,6 +1718,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 00e6bf5741..470c60a77e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3972,6 +3972,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 7e0351886b..d66ede6145 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.objectEncoding(key1); From a5f26397622bf4c9d09d30f400977f29dd96822f 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 015/236] Node: add PubSub support (#1964) Signed-off-by: Shoham Elias --- node/npm/glide/index.ts | 4 + node/src/BaseClient.ts | 315 +++++++++++++++++++++++--- 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, 611 insertions(+), 32 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 5faecf4c8a..5af5ce1ade 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -84,11 +84,12 @@ import { createSInterCard, createSInterStore, createSIsMember, - createSMembers, createSMIsMember, + createSMembers, createSMove, createSPop, createSRem, + createSUnion, createSUnionStore, createSet, createStrlen, @@ -113,16 +114,18 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, - createSUnion, } 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, @@ -266,6 +269,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: [ @@ -278,7 +286,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]) @@ -306,40 +361,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(); + } } /** @@ -351,6 +435,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; @@ -472,6 +557,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. * @@ -2947,6 +3201,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 13d4baab17..461bd4f155 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1466,6 +1466,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 96a60f3c26b75a3e032a02af09c2ae6c002c7d18 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 18 Jul 2024 10:11:09 -0700 Subject: [PATCH 016/236] Allow developers to run CI on demand (#1967) Signed-off-by: Yury-Fridlyand --- .github/workflows/csharp.yml | 1 + .github/workflows/go.yml | 3 ++- .github/workflows/java.yml | 1 + .github/workflows/node.yml | 1 + .github/workflows/python.yml | 1 + .github/workflows/rust.yml | 1 + 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 129367a362..36b380c3e0 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -24,6 +24,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9b782d8fb7..a7654519d7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -24,11 +24,12 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: + concurrency: group: go-${{ github.head_ref || github.ref }} cancel-in-progress: true - jobs: load-engine-matrix: runs-on: ubuntu-latest diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 3b18254ded..5106aec4ce 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -24,6 +24,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: java-${{ github.head_ref || github.ref }} diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index d452f05dcb..5981ad2007 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -28,6 +28,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: node-${{ github.head_ref || github.ref }} diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f364c03da3..95dadbc0a0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,6 +29,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: python-${{ github.head_ref || github.ref }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 64a8fc7aa6..2211a04f0f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,6 +22,7 @@ on: - .github/workflows/install-shared-dependencies/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: rust-${{ github.head_ref || github.ref }} From f19f4b12f907c76c01481c0b2ff93f84ad5cfbd0 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:28:13 -0700 Subject: [PATCH 017/236] Node: add ZDIFF command (#1972) * Node: add ZDIFF command Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 54 +++++++++++++++++++++ node/src/Commands.ts | 19 ++++++++ node/src/Transaction.ts | 36 ++++++++++++++ node/tests/RedisClusterClient.test.ts | 2 + node/tests/SharedTests.ts | 70 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 10 +++- 7 files changed, 191 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e304e30e..f2aec0de12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * 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 ZDIFF command ([#1972](https://github.com/valkey-io/valkey-glide/pull/1972)) * 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5af5ce1ade..904fed525a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -103,6 +103,8 @@ import { createZAdd, createZCard, createZCount, + createZDiff, + createZDiffWithScores, createZInterCard, createZInterstore, createZPopMax, @@ -2276,6 +2278,58 @@ export class BaseClient { return this.createWritePromise(createZInterCard(keys, limit)); } + /** + * Returns the difference between the first sorted set and all the successive sorted sets. + * To get the elements with their scores, see {@link zdiffWithScores}. + * + * See https://valkey.io/commands/zdiff/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets. + * @returns An `array` of elements representing the difference between the sorted sets. + * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0, "member3": 3.0}); + * await client.zadd("zset2", {"member2": 2.0}); + * await client.zadd("zset3", {"member3": 3.0}); + * const result = await client.zdiff(["zset1", "zset2", "zset3"]); + * console.log(result); // Output: ["member1"] - "member1" is in "zset1" but not "zset2" or "zset3". + * ``` + */ + public zdiff(keys: string[]): Promise { + return this.createWritePromise(createZDiff(keys)); + } + + /** + * Returns the difference between the first sorted set and all the successive sorted sets, with the associated + * scores. + * + * See https://valkey.io/commands/zdiff/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets. + * @returns A map of elements and their scores representing the difference between the sorted sets. + * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0, "member3": 3.0}); + * await client.zadd("zset2", {"member2": 2.0}); + * await client.zadd("zset3", {"member3": 3.0}); + * const result = await client.zdiffWithScores(["zset1", "zset2", "zset3"]); + * console.log(result); // Output: {"member1": 1.0} - "member1" is in "zset1" but not "zset2" or "zset3". + * ``` + */ + public zdiffWithScores(keys: string[]): Promise> { + return this.createWritePromise(createZDiffWithScores(keys)); + } + /** Returns the score of `member` in the sorted set stored at `key`. * See https://valkey.io/commands/zscore/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 461bd4f155..116a234950 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1026,6 +1026,25 @@ export function createZInterCard( return createCommand(RequestType.ZInterCard, args); } +/** + * @internal + */ +export function createZDiff(keys: string[]): command_request.Command { + const args: string[] = keys; + args.unshift(keys.length.toString()); + return createCommand(RequestType.ZDiff, args); +} + +/** + * @internal + */ +export function createZDiffWithScores(keys: string[]): command_request.Command { + const args: string[] = keys; + args.unshift(keys.length.toString()); + args.push("WITHSCORES"); + return createCommand(RequestType.ZDiff, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index d2f637f77e..10e49900e1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -113,6 +113,8 @@ import { createZAdd, createZCard, createZCount, + createZDiff, + createZDiffWithScores, createZInterCard, createZInterstore, createZPopMax, @@ -1150,6 +1152,40 @@ export class BaseTransaction> { return this.addAndReturn(createZInterCard(keys, limit)); } + /** + * Returns the difference between the first sorted set and all the successive sorted sets. + * To get the elements with their scores, see {@link zdiffWithScores}. + * + * See https://valkey.io/commands/zdiff/ for more details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - An `array` of elements representing the difference between the sorted sets. + * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * + * since Valkey version 6.2.0. + */ + public zdiff(keys: string[]): T { + return this.addAndReturn(createZDiff(keys)); + } + + /** + * Returns the difference between the first sorted set and all the successive sorted sets, with the associated + * scores. + * + * See https://valkey.io/commands/zdiff/ for more details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - A map of elements and their scores representing the difference between the sorted sets. + * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * + * since Valkey version 6.2.0. + */ + public zdiffWithScores(keys: string[]): T { + return this.addAndReturn(createZDiffWithScores(keys)); + } + /** Returns the score of `member` in the sorted set stored at `key`. * See https://valkey.io/commands/zscore/ for more details. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 2b656aea45..be9fe1cb5a 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -305,6 +305,8 @@ describe("GlideClusterClient", () => { client.sintercard(["abc", "zxy", "lkn"]), client.sinterstore("abc", ["zxy", "lkn"]), client.zinterstore("abc", ["zxy", "lkn"]), + client.zdiff(["abc", "zxy", "lkn"]), + client.zdiffWithScores(["abc", "zxy", "lkn"]), client.sunionstore("abc", ["zxy", "lkn"]), client.sunion(["abc", "zxy", "lkn"]), client.pfcount(["abc", "zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 470c60a77e..b143d287b0 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2171,6 +2171,76 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zdiff test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const key3 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + + const entries1 = { + one: 1.0, + two: 2.0, + three: 3.0, + }; + const entries2 = { two: 2.0 }; + const entries3 = { + one: 1.0, + two: 2.0, + three: 3.0, + four: 4.0, + }; + + expect(await client.zadd(key1, entries1)).toEqual(3); + expect(await client.zadd(key2, entries2)).toEqual(1); + expect(await client.zadd(key3, entries3)).toEqual(4); + + checkSimple(await client.zdiff([key1, key2])).toEqual([ + "one", + "three", + ]); + checkSimple(await client.zdiff([key1, key3])).toEqual([]); + checkSimple(await client.zdiff([nonExistingKey, key3])).toEqual( + [], + ); + + let result = await client.zdiffWithScores([key1, key2]); + const expected = { + one: 1.0, + three: 3.0, + }; + expect(compareMaps(result, expected)).toBe(true); + + result = await client.zdiffWithScores([key1, key3]); + expect(compareMaps(result, {})).toBe(true); + + result = await client.zdiffWithScores([nonExistingKey, key3]); + expect(compareMaps(result, {})).toBe(true); + + // invalid arg - key list must not be empty + await expect(client.zdiff([])).rejects.toThrow(RequestError); + await expect(client.zdiffWithScores([])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a sorted set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect(client.zdiff([stringKey, key1])).rejects.toThrow(); + await expect( + client.zdiffWithScores([stringKey, key1]), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zscore test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d66ede6145..90f6901eef 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -491,8 +491,16 @@ export async function transactionTest( args.push({ member2: 3, member3: 3.5, member4: 4, member5: 5 }); baseTransaction.zadd(key12, { one: 1, two: 2 }); args.push(2); - baseTransaction.zadd(key13, { one: 1, two: 2, tree: 3.5 }); + baseTransaction.zadd(key13, { one: 1, two: 2, three: 3.5 }); args.push(3); + + if (!(await checkIfServerVersionLessThan("6.2.0"))) { + baseTransaction.zdiff([key13, key12]); + args.push(["three"]); + baseTransaction.zdiffWithScores([key13, key12]); + args.push({ three: 3.5 }); + } + baseTransaction.zinterstore(key12, [key12, key13]); args.push(2); baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); From bc0e1b1fe23d1236ed0e534aae4aae2b14a1e3b9 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:42:03 -0700 Subject: [PATCH 018/236] Node: implement GETDEL (#1968) Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 21 +++++++++++++++++++++ node/src/Commands.ts | 7 +++++++ node/src/Transaction.ts | 14 ++++++++++++++ node/tests/SharedTests.ts | 20 ++++++++++++++++++++ node/tests/TestUtilities.ts | 4 ++++ 6 files changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2aec0de12..911d93abc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 904fed525a..99ae4af26a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -34,6 +34,7 @@ import { createExpire, createExpireAt, createGet, + createGetDel, createHDel, createHExists, createHGet, @@ -745,6 +746,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/Commands.ts b/node/src/Commands.ts index 116a234950..9fbf4ccf57 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -92,6 +92,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/src/Transaction.ts b/node/src/Transaction.ts index 10e49900e1..767b96cf55 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -39,6 +39,7 @@ import { createExpireAt, createFlushAll, createGet, + createGetDel, createHDel, createHExists, createHGet, @@ -189,6 +190,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. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b143d287b0..c38534a88f 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -513,6 +513,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 90f6901eef..43d1df5fe3 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -321,6 +321,10 @@ export async function transactionTest( args.push(0); 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 34e253cdb91e9aac97cde8710590c64df30cea29 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 18 Jul 2024 18:33:08 -0700 Subject: [PATCH 019/236] Node: Add `FUNCTION LOAD` command (#1969) * Add `FUNCTION LOAD` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/Commands.ts | 11 ++ node/src/GlideClient.ts | 31 +++++- node/src/GlideClusterClient.ts | 33 ++++++ node/src/Transaction.ts | 18 ++++ node/tests/RedisClient.test.ts | 105 ++++++++++++++++++- node/tests/RedisClusterClient.test.ts | 142 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 54 ++++++++++ 8 files changed, 393 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 911d93abc5..0c4fb488db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * 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)) +* Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) ## 1.0.0 (2024-07-09) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 9fbf4ccf57..0c61c11070 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1526,6 +1526,17 @@ export function createBLPop( return createCommand(RequestType.BLPop, args); } +/** + * @internal + */ +export function createFunctionLoad( + libraryCode: string, + replace?: boolean, +): command_request.Command { + const args = replace ? ["REPLACE", libraryCode] : [libraryCode]; + return createCommand(RequestType.FunctionLoad, args); +} + export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index a6700cd941..652a6a74bb 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -22,6 +22,7 @@ import { createCustomCommand, createDBSize, createEcho, + createFunctionLoad, createFlushAll, createInfo, createLolwut, @@ -358,7 +359,7 @@ export class GlideClient extends BaseClient { * * @example * ```typescript - * // Example usage of time method without any argument + * // Example usage of time command * const result = await client.time(); * console.log(result); // Output: ['1710925775', '913580'] * ``` @@ -385,6 +386,34 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createLolwut(options)); } + /** + * Loads a library to Valkey. + * + * See https://valkey.io/commands/function-load/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The source code that implements the library. + * @param replace - Whether the given library should overwrite a library with the same name if it + * already exists. + * @returns The library name that was loaded. + * + * @example + * ```typescript + * const code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)"; + * const result = await client.functionLoad(code, true); + * console.log(result); // Output: 'mylib' + * ``` + */ + public functionLoad( + libraryCode: string, + replace?: boolean, + ): Promise { + return this.createWritePromise( + createFunctionLoad(libraryCode, replace), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * The command will be routed to all primary nodes. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 88a2baec1d..9a6d334b6f 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -22,6 +22,7 @@ import { createCustomCommand, createDBSize, createEcho, + createFunctionLoad, createFlushAll, createInfo, createLolwut, @@ -655,6 +656,38 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Loads a library to Valkey. + * + * See https://valkey.io/commands/function-load/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The source code that implements the library. + * @param replace - Whether the given library should overwrite a library with the same name if it + * already exists. + * @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 The library name that was loaded. + * + * @example + * ```typescript + * const code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)"; + * const result = await client.functionLoad(code, true, 'allNodes'); + * console.log(result); // Output: 'mylib' + * ``` + */ + public functionLoad( + libraryCode: string, + replace?: boolean, + route?: Routes, + ): Promise { + return this.createWritePromise( + createFunctionLoad(libraryCode, replace), + toProtobufRoute(route), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 767b96cf55..866163c636 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -127,6 +127,7 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, + createFunctionLoad, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1737,6 +1738,23 @@ export class BaseTransaction> { return this.addAndReturn(createLolwut(options)); } + /** + * Loads a library to Valkey. + * + * See https://valkey.io/commands/function-load/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The source code that implements the library. + * @param replace - Whether the given library should overwrite a library with the same name if it + * already exists. + * + * Command Response - The library name that was loaded. + */ + public functionLoad(libraryCode: string, replace?: boolean): T { + return this.addAndReturn(createFunctionLoad(libraryCode, replace)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 16e2572dae..0b0b844813 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -21,11 +21,12 @@ import { } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { command_request } from "../src/ProtobufMessage"; -import { runBaseTests } from "./SharedTests"; +import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; import { checkSimple, convertStringArrayToBuffer, flushAndCloseClient, + generateLuaLibCode, getClientConfigurationOption, intoString, parseCommandLineArgs, @@ -361,6 +362,108 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function load test_%p", + async (protocol) => { + if (await checkIfServerVersionLessThan("7.0.0")) return; + + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + try { + const libName = "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + expect( + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]), + ).toEqual([]); + + checkSimple(await client.functionLoad(code)).toEqual(libName); + + checkSimple( + await client.customCommand([ + "FCALL", + funcName, + "0", + "one", + "two", + ]), + ).toEqual("one"); + checkSimple( + await client.customCommand([ + "FCALL_RO", + funcName, + "0", + "one", + "two", + ]), + ).toEqual("one"); + + // TODO verify with FUNCTION LIST + // re-load library without replace + + await expect(client.functionLoad(code)).rejects.toThrow( + `Library '${libName}' already exists`, + ); + + // re-load library with replace + checkSimple(await client.functionLoad(code, true)).toEqual( + libName, + ); + + // overwrite lib with new code + const func2Name = "myfunc2c" + uuidv4().replaceAll("-", ""); + const newCode = generateLuaLibCode( + libName, + new Map([ + [funcName, "return args[1]"], + [func2Name, "return #args"], + ]), + true, + ); + checkSimple(await client.functionLoad(newCode, true)).toEqual( + libName, + ); + + expect( + await client.customCommand([ + "FCALL", + func2Name, + "0", + "one", + "two", + ]), + ).toEqual(2); + expect( + await client.customCommand([ + "FCALL_RO", + func2Name, + "0", + "one", + "two", + ]), + ).toEqual(2); + } finally { + expect( + await client.customCommand(["FUNCTION", "FLUSH"]), + ).toEqual("OK"); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP3])("simple pubsub test", async (protocol) => { const pattern = "*"; const channel = "test-channel"; diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index be9fe1cb5a..c34dc8de2c 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -18,11 +18,15 @@ import { GlideClusterClient, InfoOptions, ProtocolVersion, + Routes, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; import { + checkClusterResponse, + checkSimple, flushAndCloseClient, + generateLuaLibCode, getClientConfigurationOption, getFirstResult, intoArray, @@ -519,6 +523,144 @@ describe("GlideClusterClient", () => { TIMEOUT, ); + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function load", + async () => { + if (await checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + const functionList = await client.customCommand( + [ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ], + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + checkSimple( + await client.functionLoad(code), + ).toEqual(libName); + // call functions from that library to confirm that it works + let fcall = await client.customCommand( + ["FCALL", funcName, "0", "one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => + checkSimple(value).toEqual("one"), + ); + + fcall = await client.customCommand( + ["FCALL_RO", funcName, "0", "one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => + checkSimple(value).toEqual("one"), + ); + + // re-load library without replace + await expect( + client.functionLoad(code), + ).rejects.toThrow( + `Library '${libName}' already exists`, + ); + + // re-load library with replace + checkSimple( + await client.functionLoad(code, true), + ).toEqual(libName); + + // overwrite lib with new code + const func2Name = + "myfunc2c" + uuidv4().replaceAll("-", ""); + const newCode = generateLuaLibCode( + libName, + new Map([ + [funcName, "return args[1]"], + [func2Name, "return #args"], + ]), + true, + ); + checkSimple( + await client.functionLoad(newCode, true), + ).toEqual(libName); + + fcall = await client.customCommand( + ["FCALL", func2Name, "0", "one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + + fcall = await client.customCommand( + ["FCALL_RO", func2Name, "0", "one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + } finally { + expect( + await client.customCommand([ + "FUNCTION", + "FLUSH", + ]), + ).toEqual("OK"); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + it.each([ [true, ProtocolVersion.RESP3], [false, ProtocolVersion.RESP3], diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 43d1df5fe3..5f5a678acd 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -202,6 +202,44 @@ export function getFirstResult( return Object.values(res).at(0); } +// TODO use matcher instead of predicate +/** Check a multi-node response from a cluster. */ +export function checkClusterMultiNodeResponse( + res: object, + predicate: (value: ReturnType) => void, +) { + for (const nodeResponse of Object.values(res)) { + predicate(nodeResponse); + } +} + +/** Check a response from a cluster. Response could be either single-node (value) or multi-node (string-value map). */ +export function checkClusterResponse( + res: object, + singleNodeRoute: boolean, + predicate: (value: ReturnType) => void, +) { + if (singleNodeRoute) predicate(res as ReturnType); + else checkClusterMultiNodeResponse(res, predicate); +} + +/** Generate a String of LUA library code. */ +export function generateLuaLibCode( + libName: string, + functions: Map, + readonly: boolean, +): string { + let code = `#!lua name=${libName}\n`; + + for (const [functionName, functionBody] of functions) { + code += `redis.register_function{ function_name = '${functionName}', callback = function(keys, args) ${functionBody} end`; + if (readonly) code += ", flags = { 'no-writes' }"; + code += " }\n"; + } + + return code; +} + /** * Parses the command-line arguments passed to the Node.js process. * @@ -570,5 +608,21 @@ export async function transactionTest( args.push(1); baseTransaction.pfcount([key11]); args.push(3); + + const libName = "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.functionLoad(code); + args.push(libName); + baseTransaction.functionLoad(code, true); + args.push(libName); + } + return args; } From 45332987763220cfef8b745df5f58f0736437123 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:37:24 -0700 Subject: [PATCH 020/236] Node: Renaming commands directory (#1981) * Node: Renaming commands directory Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- node/src/BaseClient.ts | 2 +- node/src/Commands.ts | 2 +- node/src/Transaction.ts | 2 +- node/src/{command-options => commands}/LPosOptions.ts | 0 node/tests/SharedTests.ts | 2 +- node/tests/TestUtilities.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename node/src/{command-options => commands}/LPosOptions.ts (100%) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 99ae4af26a..fc6a5af580 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -10,7 +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 { LPosOptions } from "./commands/LPosOptions"; import { AggregationType, ExpireOptions, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0c61c11070..f36e4949e6 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -4,7 +4,7 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; -import { LPosOptions } from "./command-options/LPosOptions"; +import { LPosOptions } from "./commands/LPosOptions"; import { command_request } from "./ProtobufMessage"; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 866163c636..faea761508 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,7 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { LPosOptions } from "./command-options/LPosOptions"; +import { LPosOptions } from "./commands/LPosOptions"; import { AggregationType, ExpireOptions, diff --git a/node/src/command-options/LPosOptions.ts b/node/src/commands/LPosOptions.ts similarity index 100% rename from node/src/command-options/LPosOptions.ts rename to node/src/commands/LPosOptions.ts diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c38534a88f..0ae1c7b4bd 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -28,7 +28,7 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; -import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; +import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 5f5a678acd..2724615b6d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -19,7 +19,7 @@ import { Transaction, } from ".."; import { checkIfServerVersionLessThan } from "./SharedTests"; -import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; +import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; beforeAll(() => { Logger.init("info"); From 5bef4dfe02fd26bbe5c58013d1f8442ef59177dd Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:45:30 -0700 Subject: [PATCH 021/236] Node: add SETBIT command (#1978) * Node: add SETBIT command Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 24 ++++++++++++++++++++++++ node/src/Commands.ts | 15 +++++++++++++++ node/src/Transaction.ts | 19 +++++++++++++++++++ node/tests/SharedTests.ts | 30 ++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 5 +++++ 6 files changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4fb488db..5538bba0aa 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 SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) * 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index fc6a5af580..83156ec2bd 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -81,6 +81,7 @@ import { createSCard, createSDiff, createSDiffStore, + createSetBit, createSInter, createSInterCard, createSInterStore, @@ -962,6 +963,29 @@ export class BaseClient { return this.createWritePromise(createDecrBy(key, amount)); } + /** + * Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, with + * `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less than + * `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to `value` and + * the preceding bits are set to `0`. + * + * See https://valkey.io/commands/setbit/ for more details. + * + * @param key - The key of the string. + * @param offset - The index of the bit to be set. + * @param value - The bit value to set at `offset`. The value must be `0` or `1`. + * @returns The bit value that was previously stored at `offset`. + * + * @example + * ```typescript + * const result = await client.setbit("key", 1, 1); + * console.log(result); // Output: 0 - The second bit value was 0 before setting to 1. + * ``` + */ + public setbit(key: string, offset: number, value: number): Promise { + return this.createWritePromise(createSetBit(key, offset, value)); + } + /** Retrieve the value associated with `field` in the hash stored at `key`. * See https://valkey.io/commands/hget/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f36e4949e6..a280beb159 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -442,6 +442,21 @@ export function createDecrBy( return createCommand(RequestType.DecrBy, [key, amount.toString()]); } +/** + * @internal + */ +export function createSetBit( + key: string, + offset: number, + value: number, +): command_request.Command { + return createCommand(RequestType.SetBit, [ + key, + offset.toString(), + value.toString(), + ]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index faea761508..059c01988e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -89,6 +89,7 @@ import { createSCard, createSDiff, createSDiffStore, + createSetBit, createSInter, createSInterCard, createSInterStore, @@ -375,6 +376,24 @@ export class BaseTransaction> { return this.addAndReturn(createDecrBy(key, amount)); } + /** + * Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, with + * `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less than + * `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to `value` and + * the preceding bits are set to `0`. + * + * See https://valkey.io/commands/setbit/ for more details. + * + * @param key - The key of the string. + * @param offset - The index of the bit to be set. + * @param value - The bit value to set at `offset`. The value must be `0` or `1`. + * + * Command Response - The bit value that was previously stored at `offset`. + */ + public setbit(key: string, offset: number, value: number): T { + return this.addAndReturn(createSetBit(key, offset, value)); + } + /** Reads the configuration parameters of a running Redis server. * See https://valkey.io/commands/config-get/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0ae1c7b4bd..16cd7b5246 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -488,6 +488,36 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `setbit test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + + expect(await client.setbit(key, 1, 1)).toEqual(0); + expect(await client.setbit(key, 1, 0)).toEqual(1); + + // invalid argument - offset can't be negative + await expect(client.setbit(key, -1, 1)).rejects.toThrow( + RequestError, + ); + + // invalid argument - "value" arg must be 0 or 1 + await expect(client.setbit(key, 0, 2)).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a string + expect(await client.sadd(stringKey, ["foo"])).toEqual(1); + await expect(client.setbit(stringKey, 0, 0)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set with timeout parameter_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2724615b6d..78a84ad2ab 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -350,6 +350,7 @@ export async function transactionTest( const key14 = "{key}" + uuidv4(); // sorted set const key15 = "{key}" + uuidv4(); // list const key16 = "{key}" + uuidv4(); // list + const key17 = "{key}" + uuidv4(); // bitmap const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -604,6 +605,10 @@ export async function transactionTest( args.push([key6, field + "3"]); baseTransaction.blpop([key6], 0.1); args.push([key6, field + "1"]); + + baseTransaction.setbit(key17, 1, 1); + args.push(0); + baseTransaction.pfadd(key11, ["a", "b", "c"]); args.push(1); baseTransaction.pfcount([key11]); From 815e24c1a07628a29406c8368d1c91f2ee98f091 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:23:15 -0700 Subject: [PATCH 022/236] Node: Add GEOADD (#1980) Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 34 ++++++ node/src/Commands.ts | 24 ++++ node/src/Transaction.ts | 28 +++++ node/src/commands/ConditionalChange.ts | 21 ++++ node/src/commands/geospatial/GeoAddOptions.ts | 52 ++++++++ .../src/commands/geospatial/GeospatialData.ts | 37 ++++++ node/tests/SharedTests.ts | 113 ++++++++++++++++++ node/tests/TestUtilities.ts | 10 ++ 9 files changed, 320 insertions(+) create mode 100644 node/src/commands/ConditionalChange.ts create mode 100644 node/src/commands/geospatial/GeoAddOptions.ts create mode 100644 node/src/commands/geospatial/GeospatialData.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5538bba0aa..2928591fb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ * 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)) +* Node: Added GeoAdd command ([#1980](https://github.com/valkey-io/valkey-glide/pull/1980)) #### 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/BaseClient.ts b/node/src/BaseClient.ts index 83156ec2bd..4de7b64bdf 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -33,6 +33,7 @@ import { createExists, createExpire, createExpireAt, + createGeoAdd, createGet, createGetDel, createHDel, @@ -136,6 +137,8 @@ import { connection_request, response, } from "./ProtobufMessage"; +import { GeospatialData } from "./commands/geospatial/GeospatialData"; +import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ type PromiseFunction = (value?: any) => void; @@ -3230,6 +3233,37 @@ export class BaseClient { return this.createWritePromise(createLPos(key, element, options)); } + /** + * Adds geospatial members with their positions to the specified sorted set stored at `key`. + * If a member is already a part of the sorted set, its position is updated. + * + * See https://valkey.io/commands/geoadd/ for more details. + * + * @param key - The key of the sorted set. + * @param membersToGeospatialData - A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @param options - The GeoAdd options - see {@link GeoAddOptions}. + * @returns The number of elements added to the sorted set. If `changed` is set to + * `true` in the options, returns the number of elements updated in the sorted set. + * + * @example + * ```typescript + * const options = new GeoAddOptions({updateMode: ConditionalChange.ONLY_IF_EXISTS, changed: true}); + * const num = await client.geoadd("mySortedSet", {"Palermo", new GeospatialData(13.361389, 38.115556)}, options); + * console.log(num); // Output: 1 - Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated. + * ``` + */ + public geoadd( + key: string, + membersToGeospatialData: Map, + options?: GeoAddOptions, + ): Promise { + return this.createWritePromise( + createGeoAdd(key, membersToGeospatialData, options), + ); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index a280beb159..35c34ec2b0 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -7,6 +7,8 @@ import Long from "long"; import { LPosOptions } from "./commands/LPosOptions"; import { command_request } from "./ProtobufMessage"; +import { GeospatialData } from "./commands/geospatial/GeospatialData"; +import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; import RequestType = command_request.RequestType; @@ -1767,3 +1769,25 @@ export function createLPos( export function createDBSize(): command_request.Command { return createCommand(RequestType.DBSize, []); } + +/** + * @internal + */ +export function createGeoAdd( + key: string, + membersToGeospatialData: Map, + options?: GeoAddOptions, +): command_request.Command { + let args: string[] = [key]; + + if (options) { + args = args.concat(options.toArgs()); + } + + membersToGeospatialData.forEach((coord, member) => { + args = args.concat(coord.toArgs()); + args.push(member); + }); + + return createCommand(RequestType.GeoAdd, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 059c01988e..fc07dd676d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -128,9 +128,12 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, + createGeoAdd, createFunctionLoad, } from "./Commands"; import { command_request } from "./ProtobufMessage"; +import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; +import { GeospatialData } from "./commands/geospatial/GeospatialData"; /** * Base class encompassing shared commands for both standalone and cluster mode implementations in a transaction. @@ -1816,6 +1819,31 @@ export class BaseTransaction> { public dbsize(): T { return this.addAndReturn(createDBSize()); } + + /** + * Adds geospatial members with their positions to the specified sorted set stored at `key`. + * If a member is already a part of the sorted set, its position is updated. + * + * See https://valkey.io/commands/geoadd/ for more details. + * + * @param key - The key of the sorted set. + * @param membersToGeospatialData - A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @param options - The GeoAdd options - see {@link GeoAddOptions}. + * + * Command Response - The number of elements added to the sorted set. If `changed` is set to + * `true` in the options, returns the number of elements updated in the sorted set. + */ + public geoadd( + key: string, + membersToGeospatialData: Map, + options?: GeoAddOptions, + ): T { + return this.addAndReturn( + createGeoAdd(key, membersToGeospatialData, options), + ); + } } /** diff --git a/node/src/commands/ConditionalChange.ts b/node/src/commands/ConditionalChange.ts new file mode 100644 index 0000000000..5904f90d32 --- /dev/null +++ b/node/src/commands/ConditionalChange.ts @@ -0,0 +1,21 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { BaseClient } from "src/BaseClient"; + +/** + * An optional condition to the {@link BaseClient.geoadd} command. + */ +export enum ConditionalChange { + /** + * Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Valkey API. + */ + ONLY_IF_EXISTS = "XX", + + /** + * Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Valkey API. + * */ + ONLY_IF_DOES_NOT_EXIST = "NX", +} diff --git a/node/src/commands/geospatial/GeoAddOptions.ts b/node/src/commands/geospatial/GeoAddOptions.ts new file mode 100644 index 0000000000..220dff8e19 --- /dev/null +++ b/node/src/commands/geospatial/GeoAddOptions.ts @@ -0,0 +1,52 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { ConditionalChange } from "../ConditionalChange"; + +/** + * Optional arguments for the GeoAdd command. + * + * See https://valkey.io/commands/geoadd/ for more details. + */ +export class GeoAddOptions { + /** Valkey API keyword use to modify the return value from the number of new elements added, to the total number of elements changed. */ + public static CHANGED_VALKEY_API = "CH"; + + private updateMode?: ConditionalChange; + + private changed?: boolean; + + /** + * Default constructor for GeoAddOptions. + * + * @param updateMode - Options for handling existing members. See {@link ConditionalChange}. + * @param latitude - If `true`, returns the count of changed elements instead of new elements added. + */ + constructor(options: { + updateMode?: ConditionalChange; + changed?: boolean; + }) { + this.updateMode = options.updateMode; + this.changed = options.changed; + } + + /** + * Converts GeoAddOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args: string[] = []; + + if (this.updateMode) { + args.push(this.updateMode); + } + + if (this.changed) { + args.push(GeoAddOptions.CHANGED_VALKEY_API); + } + + return args; + } +} diff --git a/node/src/commands/geospatial/GeospatialData.ts b/node/src/commands/geospatial/GeospatialData.ts new file mode 100644 index 0000000000..63c15bdff0 --- /dev/null +++ b/node/src/commands/geospatial/GeospatialData.ts @@ -0,0 +1,37 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +/** + * Represents a geographic position defined by longitude and latitude. + * The exact limits, as specified by `EPSG:900913 / EPSG:3785 / OSGEO:41001` are the + * following: + * + * Valid longitudes are from `-180` to `180` degrees. + * Valid latitudes are from `-85.05112878` to `85.05112878` degrees. + */ +export class GeospatialData { + private longitude: number; + + private latitude: number; + + /** + * Default constructor for GeospatialData. + * + * @param longitude - The longitude coordinate. + * @param latitude - The latitude coordinate. + */ + constructor(longitude: number, latitude: number) { + this.longitude = longitude; + this.latitude = latitude; + } + + /** + * Converts GeospatialData into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + return [this.longitude.toString(), this.latitude.toString()]; + } +} diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 16cd7b5246..ed215b599c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,6 +29,9 @@ import { } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; +import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; +import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; +import { ConditionalChange } from "../build-ts/src/commands/ConditionalChange"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { @@ -4140,6 +4143,116 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geoadd test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const membersToCoordinates = new Map(); + membersToCoordinates.set( + "Palermo", + new GeospatialData(13.361389, 38.115556), + ); + membersToCoordinates.set( + "Catania", + new GeospatialData(15.087269, 37.502669), + ); + + // default geoadd + expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); + + // with update mode options + membersToCoordinates.set( + "Catania", + new GeospatialData(15.087269, 39), + ); + expect( + await client.geoadd( + key1, + membersToCoordinates, + new GeoAddOptions({ + updateMode: + ConditionalChange.ONLY_IF_DOES_NOT_EXIST, + }), + ), + ).toBe(0); + expect( + await client.geoadd( + key1, + membersToCoordinates, + new GeoAddOptions({ + updateMode: ConditionalChange.ONLY_IF_EXISTS, + }), + ), + ).toBe(0); + + // with changed option + membersToCoordinates.set( + "Catania", + new GeospatialData(15.087269, 40), + ); + membersToCoordinates.set( + "Tel-Aviv", + new GeospatialData(32.0853, 34.7818), + ); + expect( + await client.geoadd( + key1, + membersToCoordinates, + new GeoAddOptions({ changed: true }), + ), + ).toBe(2); + + // key exists but holding non-zset value + expect(await client.set(key2, "foo")).toBe("OK"); + await expect( + client.geoadd(key2, membersToCoordinates), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geoadd invalid args test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + + // empty coordinate map + await expect(client.geoadd(key, new Map())).rejects.toThrow(); + + // coordinate out of bound + await expect( + client.geoadd( + key, + new Map([["Place", new GeospatialData(-181, 0)]]), + ), + ).rejects.toThrow(); + await expect( + client.geoadd( + key, + new Map([["Place", new GeospatialData(181, 0)]]), + ), + ).rejects.toThrow(); + await expect( + client.geoadd( + key, + new Map([["Place", new GeospatialData(0, 86)]]), + ), + ).rejects.toThrow(); + await expect( + client.geoadd( + key, + new Map([["Place", new GeospatialData(0, -86)]]), + ), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 78a84ad2ab..6057c44723 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -20,6 +20,7 @@ import { } from ".."; import { checkIfServerVersionLessThan } from "./SharedTests"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; +import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; beforeAll(() => { Logger.init("info"); @@ -351,6 +352,7 @@ export async function transactionTest( const key15 = "{key}" + uuidv4(); // list const key16 = "{key}" + uuidv4(); // list const key17 = "{key}" + uuidv4(); // bitmap + const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -613,6 +615,14 @@ export async function transactionTest( args.push(1); baseTransaction.pfcount([key11]); args.push(3); + baseTransaction.geoadd( + key18, + new Map([ + ["Palermo", new GeospatialData(13.361389, 38.115556)], + ["Catania", new GeospatialData(15.087269, 37.502669)], + ]), + ); + args.push(2); const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); From 37899dfea1e15e2a55bc5c45ad2e41179b459c68 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:30:05 -0700 Subject: [PATCH 023/236] Node: add ZDIFFSTORE command (#1985) * Node: add ZDIFFSTORE command Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 30 ++++++++++ node/src/Commands.ts | 11 ++++ node/src/Transaction.ts | 19 +++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 80 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 7 files changed, 144 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2928591fb9..a5120dc152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * 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 ZDIFF command ([#1972](https://github.com/valkey-io/valkey-glide/pull/1972)) +* Node: Added ZDIFFSTORE command ([#1985](https://github.com/valkey-io/valkey-glide/pull/1985)) * 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4de7b64bdf..5bf3f49ca9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -107,6 +107,7 @@ import { createZCard, createZCount, createZDiff, + createZDiffStore, createZDiffWithScores, createZInterCard, createZInterstore, @@ -2378,6 +2379,35 @@ export class BaseClient { return this.createWritePromise(createZDiffWithScores(keys)); } + /** + * Calculates the difference between the first sorted set and all the successive sorted sets in `keys` and stores + * the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are + * treated as empty sets. + * + * See https://valkey.io/commands/zdiffstore/ for more details. + * + * @remarks When in cluster mode, all keys in `keys` and `destination` must map to the same hash slot. + * @param destination - The key for the resulting sorted set. + * @param keys - The keys of the sorted sets to compare. + * @returns The number of members in the resulting sorted set stored at `destination`. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0}); + * await client.zadd("zset2", {"member1": 1.0}); + * const result1 = await client.zdiffstore("zset3", ["zset1", "zset2"]); + * console.log(result1); // Output: 1 - One member exists in "key1" but not "key2", and this member was stored in "zset3". + * + * const result2 = await client.zrange("zset3", {start: 0, stop: -1}); + * console.log(result2); // Output: ["member2"] - "member2" is now stored in "my_sorted_set". + * ``` + */ + public zdiffstore(destination: string, keys: string[]): Promise { + return this.createWritePromise(createZDiffStore(destination, keys)); + } + /** Returns the score of `member` in the sorted set stored at `key`. * See https://valkey.io/commands/zscore/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 35c34ec2b0..fc74155b42 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1069,6 +1069,17 @@ export function createZDiffWithScores(keys: string[]): command_request.Command { return createCommand(RequestType.ZDiff, args); } +/** + * @internal + */ +export function createZDiffStore( + destination: string, + keys: string[], +): command_request.Command { + const args: string[] = [destination, keys.length.toString(), ...keys]; + return createCommand(RequestType.ZDiffStore, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index fc07dd676d..3f0bfc1166 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -116,6 +116,7 @@ import { createZCard, createZCount, createZDiff, + createZDiffStore, createZDiffWithScores, createZInterCard, createZInterstore, @@ -1223,6 +1224,24 @@ export class BaseTransaction> { return this.addAndReturn(createZDiffWithScores(keys)); } + /** + * Calculates the difference between the first sorted set and all the successive sorted sets in `keys` and stores + * the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are + * treated as empty sets. + * + * See https://valkey.io/commands/zdiffstore/ for more details. + * + * @param destination - The key for the resulting sorted set. + * @param keys - The keys of the sorted sets to compare. + * + * Command Response - The number of members in the resulting sorted set stored at `destination`. + * + * since Valkey version 6.2.0. + */ + public zdiffstore(destination: string, keys: string[]): T { + return this.addAndReturn(createZDiffStore(destination, keys)); + } + /** Returns the score of `member` in the sorted set stored at `key`. * See https://valkey.io/commands/zscore/ for more details. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index c34dc8de2c..bec58865ea 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -311,6 +311,7 @@ describe("GlideClusterClient", () => { client.zinterstore("abc", ["zxy", "lkn"]), client.zdiff(["abc", "zxy", "lkn"]), client.zdiffWithScores(["abc", "zxy", "lkn"]), + client.zdiffstore("abc", ["zxy", "lkn"]), client.sunionstore("abc", ["zxy", "lkn"]), client.sunion(["abc", "zxy", "lkn"]), client.pfcount(["abc", "zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ed215b599c..d477b73537 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2294,6 +2294,86 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zdiffstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const key3 = `{key}-${uuidv4()}`; + const key4 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + + const entries1 = { + one: 1.0, + two: 2.0, + three: 3.0, + }; + const entries2 = { two: 2.0 }; + const entries3 = { + one: 1.0, + two: 2.0, + three: 3.0, + four: 4.0, + }; + + expect(await client.zadd(key1, entries1)).toEqual(3); + expect(await client.zadd(key2, entries2)).toEqual(1); + expect(await client.zadd(key3, entries3)).toEqual(4); + + expect(await client.zdiffstore(key4, [key1, key2])).toEqual(2); + const result1 = await client.zrangeWithScores(key4, { + start: 0, + stop: -1, + }); + const expected1 = { one: 1.0, three: 3.0 }; + expect(compareMaps(result1, expected1)).toBe(true); + + expect( + await client.zdiffstore(key4, [key3, key2, key1]), + ).toEqual(1); + const result2 = await client.zrangeWithScores(key4, { + start: 0, + stop: -1, + }); + expect(compareMaps(result2, { four: 4.0 })).toBe(true); + + expect(await client.zdiffstore(key4, [key1, key3])).toEqual(0); + const result3 = await client.zrangeWithScores(key4, { + start: 0, + stop: -1, + }); + expect(compareMaps(result3, {})).toBe(true); + + expect( + await client.zdiffstore(key4, [nonExistingKey, key1]), + ).toEqual(0); + const result4 = await client.zrangeWithScores(key4, { + start: 0, + stop: -1, + }); + expect(compareMaps(result4, {})).toBe(true); + + // invalid arg - key list must not be empty + await expect(client.zdiffstore(key4, [])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a sorted set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.zdiffstore(key4, [stringKey, key1]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zscore test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6057c44723..4550d4b4cc 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -544,6 +544,8 @@ export async function transactionTest( args.push(["three"]); baseTransaction.zdiffWithScores([key13, key12]); args.push({ three: 3.5 }); + baseTransaction.zdiffstore(key13, [key13, key13]); + args.push(0); } baseTransaction.zinterstore(key12, [key12, key13]); From 2ecad75cbcfd1b25a826fc8274cdd1bebc9da2cd Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:40:24 -0700 Subject: [PATCH 024/236] Node: Add command ZRevRank (#1977) * Node: Add command ZRevRank Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 51 +++++++++++++++++++++++++++++++++++++ node/src/Commands.ts | 20 +++++++++++++++ node/src/Transaction.ts | 38 +++++++++++++++++++++++++++ node/tests/SharedTests.ts | 44 ++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 8 ++++++ 6 files changed, 162 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5120dc152..f58d85d0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,7 @@ * 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)) * Node: Added GeoAdd command ([#1980](https://github.com/valkey-io/valkey-glide/pull/1980)) +* Node: Added ZRevRank command ([#1977](https://github.com/valkey-io/valkey-glide/pull/1977)) #### 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/BaseClient.ts b/node/src/BaseClient.ts index 5bf3f49ca9..0abedf6e5f 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -119,6 +119,8 @@ import { createZRem, createZRemRangeByRank, createZRemRangeByScore, + createZRevRank, + createZRevRankWithScore, createZScore, } from "./Commands"; import { @@ -2862,6 +2864,55 @@ export class BaseClient { return this.createWritePromise(createZRank(key, member, true)); } + /** + * Returns the rank of `member` in the sorted set stored at `key`, where + * scores are ordered from the highest to lowest, starting from 0. + * To get the rank of `member` with its score, see {@link zrevrankWithScore}. + * + * See https://valkey.io/commands/zrevrank/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose rank is to be retrieved. + * @returns The rank of `member` in the sorted set, where ranks are ordered from high to low based on scores. + * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. + * + * @example + * ```typescript + * const result = await client.zrevrank("my_sorted_set", "member2"); + * console.log(result); // Output: 1 - Indicates that "member2" has the second-highest score in the sorted set "my_sorted_set". + * ``` + */ + public zrevrank(key: string, member: string): Promise { + return this.createWritePromise(createZRevRank(key, member)); + } + + /** + * Returns the rank of `member` in the sorted set stored at `key` with its + * score, where scores are ordered from the highest to lowest, starting from 0. + * + * See https://valkey.io/commands/zrevrank/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose rank is to be retrieved. + * @returns A list containing the rank and score of `member` in the sorted set, where ranks + * are ordered from high to low based on scores. + * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. + * + * since - Valkey version 7.2.0. + * + * @example + * ```typescript + * const result = await client.zrevankWithScore("my_sorted_set", "member2"); + * console.log(result); // Output: [1, 6.0] - Indicates that "member2" with score 6.0 has the second-highest score in the sorted set "my_sorted_set". + * ``` + */ + public zrevrankWithScore( + key: string, + member: string, + ): Promise<(number[] | null)[]> { + return this.createWritePromise(createZRevRankWithScore(key, member)); + } + /** * Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. * See https://valkey.io/commands/xadd/ for more details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index fc74155b42..78af65bd62 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1802,3 +1802,23 @@ export function createGeoAdd( return createCommand(RequestType.GeoAdd, args); } + +/** + * @internal + */ +export function createZRevRank( + key: string, + member: string, +): command_request.Command { + return createCommand(RequestType.ZRevRank, [key, member]); +} + +/** + * @internal + */ +export function createZRevRankWithScore( + key: string, + member: string, +): command_request.Command { + return createCommand(RequestType.ZRevRank, [key, member, "WITHSCORE"]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3f0bfc1166..1192bb052f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -130,6 +130,8 @@ import { createZRemRangeByScore, createZScore, createGeoAdd, + createZRevRank, + createZRevRankWithScore, createFunctionLoad, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1493,6 +1495,42 @@ export class BaseTransaction> { return this.addAndReturn(createZRank(key, member, true)); } + /** + * Returns the rank of `member` in the sorted set stored at `key`, where + * scores are ordered from the highest to lowest, starting from 0. + * To get the rank of `member` with its score, see {@link zrevrankWithScore}. + * + * See https://valkey.io/commands/zrevrank/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose rank is to be retrieved. + * + * Command Response - The rank of `member` in the sorted set, where ranks are ordered from high to low based on scores. + * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. + */ + public zrevrank(key: string, member: string): T { + return this.addAndReturn(createZRevRank(key, member)); + } + + /** + * Returns the rank of `member` in the sorted set stored at `key` with its + * score, where scores are ordered from the highest to lowest, starting from 0. + * + * See https://valkey.io/commands/zrevrank/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose rank is to be retrieved. + * + * Command Response - A list containing the rank and score of `member` in the sorted set, where ranks + * are ordered from high to low based on scores. + * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. + * + * since - Valkey version 7.2.0. + */ + public zrevrankWithScore(key: string, member: string): T { + return this.addAndReturn(createZRevRankWithScore(key, member)); + } + /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). * See https://valkey.io/commands/persist/ for more details. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d477b73537..d646835084 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3120,6 +3120,50 @@ export function runBaseTests(config: { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrevrank test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonSetKey = uuidv4(); + const membersScores = { one: 1.5, two: 2, three: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + expect(await client.zrevrank(key, "three")).toEqual(0); + + if (!(await checkIfServerVersionLessThan("7.2.0"))) { + expect(await client.zrevrankWithScore(key, "one")).toEqual([ + 2, 1.5, + ]); + expect( + await client.zrevrankWithScore( + key, + "nonExistingMember", + ), + ).toBeNull(); + expect( + await client.zrevrankWithScore( + "nonExistingKey", + "member", + ), + ).toBeNull(); + } + + expect( + await client.zrevrank(key, "nonExistingMember"), + ).toBeNull(); + expect( + await client.zrevrank("nonExistingKey", "member"), + ).toBeNull(); + + // Key exists, but is not a sorted set + checkSimple(await client.set(nonSetKey, "value")).toEqual("OK"); + await expect( + client.zrevrank(nonSetKey, "member"), + ).rejects.toThrow(); + }, protocol); + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `test brpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4550d4b4cc..2c508cc2ca 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -522,6 +522,14 @@ export async function transactionTest( args.push([0, 1]); } + baseTransaction.zrevrank(key8, "member5"); + args.push(0); + + if (!(await checkIfServerVersionLessThan("7.2.0"))) { + baseTransaction.zrevrankWithScore(key8, "member5"); + args.push([0, 5]); + } + baseTransaction.zaddIncr(key8, "member2", 1); args.push(3); baseTransaction.zrem(key8, ["member1"]); From 05bb0aed2e4d1fecc82114b64d0846fa11c93cbf Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 19 Jul 2024 15:27:40 -0700 Subject: [PATCH 025/236] Node: Add `FLUSHDB` command. (#1986) * Add `FLUSHDB` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 ++ node/src/Commands.ts | 28 +++++++++-------------- node/src/GlideClient.ts | 30 ++++++++++++++++++------- node/src/GlideClusterClient.ts | 32 +++++++++++++++++++++++---- node/src/Transaction.ts | 27 +++++++++++++++++----- node/src/commands/FlushMode.ts | 23 +++++++++++++++++++ node/tests/RedisClient.test.ts | 18 +++++++++------ node/tests/RedisClusterClient.test.ts | 29 ++++++++++++++++++++++++ node/tests/SharedTests.ts | 2 +- node/tests/TestUtilities.ts | 7 ++++++ 11 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 node/src/commands/FlushMode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f58d85d0f6..7551169053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) * Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index cc181ad00e..d3a2d6af84 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -87,6 +87,7 @@ function initialize() { Logger, LPosOptions, ExpireOptions, + FlushMode, InfoOptions, InsertPosition, SetOptions, @@ -133,6 +134,7 @@ function initialize() { Logger, LPosOptions, ExpireOptions, + FlushMode, InfoOptions, InsertPosition, SetOptions, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 78af65bd62..73bf204452 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -5,6 +5,7 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; import { LPosOptions } from "./commands/LPosOptions"; +import { FlushMode } from "./commands/FlushMode"; import { command_request } from "./ProtobufMessage"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; @@ -1729,31 +1730,24 @@ export function createLolwut(options?: LolwutOptions): command_request.Command { } /** - * Defines flushing mode for: - * - * `FLUSHALL` command. - * - * See https://valkey.io/commands/flushall/ for details. + * @internal */ -export enum FlushMode { - /** - * Flushes synchronously. - * - * since Valkey 6.2 and above. - */ - SYNC = "SYNC", - /** Flushes asynchronously. */ - ASYNC = "ASYNC", +export function createFlushAll(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FlushAll, [mode.toString()]); + } else { + return createCommand(RequestType.FlushAll, []); + } } /** * @internal */ -export function createFlushAll(mode?: FlushMode): command_request.Command { +export function createFlushDB(mode?: FlushMode): command_request.Command { if (mode) { - return createCommand(RequestType.FlushAll, [mode.toString()]); + return createCommand(RequestType.FlushDB, [mode.toString()]); } else { - return createCommand(RequestType.FlushAll, []); + return createCommand(RequestType.FlushDB, []); } } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 652a6a74bb..0ca4b477b7 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -10,7 +10,6 @@ import { ReturnType, } from "./BaseClient"; import { - FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -22,8 +21,9 @@ import { createCustomCommand, createDBSize, createEcho, - createFunctionLoad, createFlushAll, + createFlushDB, + createFunctionLoad, createInfo, createLolwut, createPing, @@ -33,6 +33,7 @@ import { } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; +import { FlushMode } from "./commands/FlushMode"; /* eslint-disable-next-line @typescript-eslint/no-namespace */ export namespace GlideClientConfiguration { @@ -416,7 +417,6 @@ export class GlideClient extends BaseClient { /** * 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. * @@ -430,11 +430,25 @@ export class GlideClient extends BaseClient { * ``` */ public flushall(mode?: FlushMode): Promise { - if (mode) { - return this.createWritePromise(createFlushAll(mode)); - } else { - return this.createWritePromise(createFlushAll()); - } + return this.createWritePromise(createFlushAll(mode)); + } + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * See https://valkey.io/commands/flushdb/ 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.flushdb(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushdb(mode?: FlushMode): Promise { + return this.createWritePromise(createFlushDB(mode)); } /** diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 9a6d334b6f..24f7e48579 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -10,7 +10,6 @@ import { ReturnType, } from "./BaseClient"; import { - FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -22,14 +21,16 @@ import { createCustomCommand, createDBSize, createEcho, - createFunctionLoad, createFlushAll, + createFlushDB, + createFunctionLoad, createInfo, createLolwut, createPing, createPublish, createTime, } from "./Commands"; +import { FlushMode } from "./commands/FlushMode"; import { RequestError } from "./Errors"; import { command_request, connection_request } from "./ProtobufMessage"; import { ClusterTransaction } from "./Transaction"; @@ -694,8 +695,8 @@ export class GlideClusterClient extends BaseClient { * 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`. + * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. * @returns `OK`. * * @example @@ -711,6 +712,29 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * See https://valkey.io/commands/flushdb/ 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 primary nodes, 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.flushdb(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushdb(mode?: FlushMode, route?: Routes): Promise { + return this.createWritePromise( + createFlushDB(mode), + toProtobufRoute(route), + ); + } + /** * Returns the number of keys in the database. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 1192bb052f..3c94a761f8 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,11 +2,9 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { LPosOptions } from "./commands/LPosOptions"; import { AggregationType, ExpireOptions, - FlushMode, InfoOptions, InsertPosition, KeyWeight, @@ -38,6 +36,9 @@ import { createExpire, createExpireAt, createFlushAll, + createFlushDB, + createFunctionLoad, + createGeoAdd, createGet, createGetDel, createHDel, @@ -89,13 +90,12 @@ import { createSCard, createSDiff, createSDiffStore, - createSetBit, createSInter, createSInterCard, createSInterStore, createSIsMember, - createSMembers, createSMIsMember, + createSMembers, createSMove, createSPop, createSRem, @@ -103,6 +103,7 @@ import { createSUnionStore, createSelect, createSet, + createSetBit, createStrlen, createTTL, createTime, @@ -129,12 +130,12 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, - createGeoAdd, createZRevRank, createZRevRankWithScore, - createFunctionLoad, } from "./Commands"; import { command_request } from "./ProtobufMessage"; +import { FlushMode } from "./commands/FlushMode"; +import { LPosOptions } from "./commands/LPosOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; @@ -1840,12 +1841,26 @@ export class BaseTransaction> { * 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)); } + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * See https://valkey.io/commands/flushdb/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * + * Command Response - `OK`. + */ + public flushdb(mode?: FlushMode): T { + return this.addAndReturn(createFlushDB(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 diff --git a/node/src/commands/FlushMode.ts b/node/src/commands/FlushMode.ts new file mode 100644 index 0000000000..78b9ca10c0 --- /dev/null +++ b/node/src/commands/FlushMode.ts @@ -0,0 +1,23 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +// Import below added to fix up the TSdoc link, but eslint blames for unused import. +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { GlideClient } from "src/GlideClient"; + +/** + * Defines flushing mode for {@link GlideClient.flushall|flushall} and {@link GlideClient.flushdb|flushdb} commands. + * + * See https://valkey.io/commands/flushall/ and https://valkey.io/commands/flushdb/ for details. + */ +export enum FlushMode { + /** + * Flushes synchronously. + * + * since Valkey version 6.2.0. + */ + SYNC = "SYNC", + /** Flushes asynchronously. */ + ASYNC = "ASYNC", +} diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 0b0b844813..b97d87f7b4 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -33,6 +33,7 @@ import { parseEndpoints, transactionTest, } from "./TestUtilities"; +import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; /* eslint-disable @typescript-eslint/no-var-requires */ @@ -130,26 +131,29 @@ describe("GlideClient", () => { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "simple select test", + "select dbsize flushdb test %p", async (protocol) => { client = await GlideClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), ); - let selectResult = await client.select(0); - checkSimple(selectResult).toEqual("OK"); + checkSimple(await client.select(0)).toEqual("OK"); const key = uuidv4(); const value = uuidv4(); const result = await client.set(key, value); checkSimple(result).toEqual("OK"); - selectResult = await client.select(1); - checkSimple(selectResult).toEqual("OK"); + checkSimple(await client.select(1)).toEqual("OK"); expect(await client.get(key)).toEqual(null); + checkSimple(await client.flushdb()).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); - selectResult = await client.select(0); - checkSimple(selectResult).toEqual("OK"); + checkSimple(await client.select(0)).toEqual("OK"); checkSimple(await client.get(key)).toEqual(value); + + expect(await client.dbsize()).toBeGreaterThan(0); + checkSimple(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); }, ); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index bec58865ea..1158bd1513 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -35,6 +35,7 @@ import { parseEndpoints, transactionTest, } from "./TestUtilities"; +import { FlushMode } from "../build-ts/src/commands/FlushMode"; type Context = { client: GlideClusterClient; }; @@ -524,6 +525,34 @@ describe("GlideClusterClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "flushdb flushall dbsize test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + expect(await client.dbsize()).toBeGreaterThanOrEqual(0); + checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toBeGreaterThan(0); + + checkSimple(await client.flushall()).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + checkSimple(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + checkSimple(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + client.close(); + }, + ); + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "Protocol is RESP2 = %s", (protocol) => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d646835084..8ef475b5cf 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -8,7 +8,6 @@ import { v4 as uuidv4 } from "uuid"; import { ClosingError, ExpireOptions, - FlushMode, GlideClient, GlideClusterClient, InfoOptions, @@ -32,6 +31,7 @@ import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; import { ConditionalChange } from "../build-ts/src/commands/ConditionalChange"; +import { FlushMode } from "../build-ts/src/commands/FlushMode"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2c508cc2ca..3af1e1dfb2 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -21,6 +21,7 @@ import { import { checkIfServerVersionLessThan } from "./SharedTests"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; +import { FlushMode } from "../build-ts/src/commands/FlushMode"; beforeAll(() => { Logger.init("info"); @@ -358,6 +359,12 @@ export async function transactionTest( const args: ReturnType[] = []; baseTransaction.flushall(); args.push("OK"); + baseTransaction.flushall(FlushMode.SYNC); + args.push("OK"); + baseTransaction.flushdb(); + args.push("OK"); + baseTransaction.flushdb(FlushMode.SYNC); + args.push("OK"); baseTransaction.dbsize(); args.push(0); baseTransaction.set(key1, "bar"); From 7ba692d43886900417c4620d1ce38317b6ad0ccb Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Fri, 19 Jul 2024 18:29:14 -0700 Subject: [PATCH 026/236] Node: add `FUNCTION FLUSH` command (#1984) * Node: add FUNCTION FLUSH command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/Commands.ts | 11 +++ node/src/GlideClient.ts | 21 +++++ node/src/GlideClusterClient.ts | 26 ++++++ node/src/Transaction.ts | 15 ++++ node/tests/RedisClient.test.ts | 63 +++++++++++++- node/tests/RedisClusterClient.test.ts | 114 +++++++++++++++++++++++++- node/tests/TestUtilities.ts | 10 ++- 8 files changed, 252 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7551169053..6dd54d8989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * 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)) * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) +* Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) ## 1.0.0 (2024-07-09) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 73bf204452..b8055331ad 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1566,6 +1566,17 @@ export function createFunctionLoad( return createCommand(RequestType.FunctionLoad, args); } +/** + * @internal + */ +export function createFunctionFlush(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FunctionFlush, [mode.toString()]); + } else { + return createCommand(RequestType.FunctionFlush, []); + } +} + export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 0ca4b477b7..614ea9f260 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -23,6 +23,7 @@ import { createEcho, createFlushAll, createFlushDB, + createFunctionFlush, createFunctionLoad, createInfo, createLolwut, @@ -415,6 +416,26 @@ export class GlideClient extends BaseClient { ); } + /** + * Deletes all function libraries. + * + * See https://valkey.io/commands/function-flush/ for details. + * + * since Valkey version 7.0.0. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns A simple OK response. + * + * @example + * ```typescript + * const result = await client.functionFlush(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public functionFlush(mode?: FlushMode): Promise { + return this.createWritePromise(createFunctionFlush(mode)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 24f7e48579..d583fd4c89 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -23,6 +23,7 @@ import { createEcho, createFlushAll, createFlushDB, + createFunctionFlush, createFunctionLoad, createInfo, createLolwut, @@ -689,6 +690,31 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Deletes all function libraries. + * + * See https://valkey.io/commands/function-flush/ for details. + * + * since Valkey version 7.0.0. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @param route - The command will be routed to all primary node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns A simple OK response. + * + * @example + * ```typescript + * const result = await client.functionFlush(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public functionFlush(mode?: FlushMode, route?: Routes): Promise { + return this.createWritePromise( + createFunctionFlush(mode), + toProtobufRoute(route), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3c94a761f8..354ce277df 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -37,6 +37,7 @@ import { createExpireAt, createFlushAll, createFlushDB, + createFunctionFlush, createFunctionLoad, createGeoAdd, createGet, @@ -1835,6 +1836,20 @@ export class BaseTransaction> { return this.addAndReturn(createFunctionLoad(libraryCode, replace)); } + /** + * Deletes all function libraries. + * + * See https://valkey.io/commands/function-flush/ for details. + * + * since Valkey version 7.0.0. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * Command Response - `OK`. + */ + public functionFlush(mode?: FlushMode): T { + return this.addAndReturn(createFunctionFlush(mode)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index b97d87f7b4..37c4f5c797 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -20,6 +20,7 @@ import { Transaction, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; import { command_request } from "../src/ProtobufMessage"; import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; import { @@ -33,7 +34,6 @@ import { parseEndpoints, transactionTest, } from "./TestUtilities"; -import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; /* eslint-disable @typescript-eslint/no-var-requires */ @@ -460,9 +460,66 @@ describe("GlideClient", () => { ]), ).toEqual(2); } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function flush test_%p", + async (protocol) => { + if (await checkIfServerVersionLessThan("7.0.0")) return; + + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + try { + const libName = "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist expect( - await client.customCommand(["FUNCTION", "FLUSH"]), - ).toEqual("OK"); + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]), + ).toEqual([]); + + checkSimple(await client.functionLoad(code)).toEqual(libName); + + // Flush functions + expect(await client.functionFlush(FlushMode.SYNC)).toEqual( + "OK", + ); + expect(await client.functionFlush(FlushMode.ASYNC)).toEqual( + "OK", + ); + + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + expect( + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]), + ).toEqual([]); + + // Attempt to re-load library without overwriting to ensure FLUSH was effective + checkSimple(await client.functionLoad(code)).toEqual(libName); + } finally { + expect(await client.functionFlush()).toEqual("OK"); client.close(); } }, diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 1158bd1513..b8b3008ca2 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -21,6 +21,7 @@ import { Routes, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -35,7 +36,6 @@ import { parseEndpoints, transactionTest, } from "./TestUtilities"; -import { FlushMode } from "../build-ts/src/commands/FlushMode"; type Context = { client: GlideClusterClient; }; @@ -675,12 +675,118 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual(2), ); } finally { - expect( + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function flush", + async () => { + if (await checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + const functionList1 = await client.customCommand([ "FUNCTION", - "FLUSH", - ]), + "LIST", + "LIBRARYNAME", + libName, + ]); + checkClusterResponse( + functionList1 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // load the library + checkSimple( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // flush functions + expect( + await client.functionFlush( + FlushMode.SYNC, + route, + ), + ).toEqual("OK"); + expect( + await client.functionFlush( + FlushMode.ASYNC, + route, + ), ).toEqual("OK"); + + // TODO use commands instead of customCommand once implemented + // verify function does not exist + const functionList2 = + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]); + checkClusterResponse( + functionList2 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Attempt to re-load library without overwriting to ensure FLUSH was effective + checkSimple( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); client.close(); } }, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 3af1e1dfb2..da4233ae7b 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -18,10 +18,10 @@ import { ReturnType, Transaction, } from ".."; -import { checkIfServerVersionLessThan } from "./SharedTests"; +import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; -import { FlushMode } from "../build-ts/src/commands/FlushMode"; +import { checkIfServerVersionLessThan } from "./SharedTests"; beforeAll(() => { Logger.init("info"); @@ -654,6 +654,12 @@ export async function transactionTest( args.push(libName); baseTransaction.functionLoad(code, true); args.push(libName); + baseTransaction.functionFlush(); + args.push("OK"); + baseTransaction.functionFlush(FlushMode.ASYNC); + args.push("OK"); + baseTransaction.functionFlush(FlushMode.SYNC); + args.push("OK"); } return args; From b7d5337fa7c1458bc16cec4ba4bd868e20798ece Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:39:48 -0700 Subject: [PATCH 027/236] Node: added ZMSCORE command (#1987) * Node: added ZMSCORE command Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 23 +++++++++++++++++ node/src/Commands.ts | 10 ++++++++ node/src/Transaction.ts | 18 ++++++++++++++ node/tests/SharedTests.ts | 49 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 103 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd54d8989..10456b9734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * 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 ZMSCORE command ([#1987](https://github.com/valkey-io/valkey-glide/pull/1987)) * 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)) * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0abedf6e5f..2b8e3f5733 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -111,6 +111,7 @@ import { createZDiffWithScores, createZInterCard, createZInterstore, + createZMScore, createZPopMax, createZPopMin, createZRange, @@ -2444,6 +2445,28 @@ export class BaseClient { return this.createWritePromise(createZScore(key, member)); } + /** + * Returns the scores associated with the specified `members` in the sorted set stored at `key`. + * + * See https://valkey.io/commands/zmscore/ for more details. + * + * @param key - The key of the sorted set. + * @param members - A list of members in the sorted set. + * @returns An `array` of scores corresponding to `members`. + * If a member does not exist in the sorted set, the corresponding value in the list will be `null`. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * const result = await client.zmscore("zset1", ["member1", "non_existent_member", "member2"]); + * console.log(result); // Output: [1.0, null, 2.0] - "member1" has a score of 1.0, "non_existent_member" does not exist in the sorted set, and "member2" has a score of 2.0. + * ``` + */ + public zmscore(key: string, members: string[]): Promise<(number | null)[]> { + return this.createWritePromise(createZMScore(key, members)); + } + /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. * See https://valkey.io/commands/zcount/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b8055331ad..190f39b1bd 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1091,6 +1091,16 @@ export function createZScore( return createCommand(RequestType.ZScore, [key, member]); } +/** + * @internal + */ +export function createZMScore( + key: string, + members: string[], +): command_request.Command { + return createCommand(RequestType.ZMScore, [key, ...members]); +} + export type ScoreBoundary = /** * Positive infinity bound for sorted set. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 354ce277df..63eeecf6e8 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -122,6 +122,7 @@ import { createZDiffWithScores, createZInterCard, createZInterstore, + createZMScore, createZPopMax, createZPopMin, createZRange, @@ -1260,6 +1261,23 @@ export class BaseTransaction> { return this.addAndReturn(createZScore(key, member)); } + /** + * Returns the scores associated with the specified `members` in the sorted set stored at `key`. + * + * See https://valkey.io/commands/zmscore/ for more details. + * + * @param key - The key of the sorted set. + * @param members - A list of members in the sorted set. + * + * Command Response - An `array` of scores corresponding to `members`. + * If a member does not exist in the sorted set, the corresponding value in the list will be `null`. + * + * since Valkey version 6.2.0. + */ + public zmscore(key: string, members: string[]): T { + return this.addAndReturn(createZMScore(key, members)); + } + /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. * See https://valkey.io/commands/zcount/ for more details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 8ef475b5cf..40c4636f74 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2397,6 +2397,55 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zmscore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + + const entries = { + one: 1.0, + two: 2.0, + three: 3.0, + }; + expect(await client.zadd(key1, entries)).toEqual(3); + + expect( + await client.zmscore(key1, ["one", "three", "two"]), + ).toEqual([1.0, 3.0, 2.0]); + expect( + await client.zmscore(key1, [ + "one", + "nonExistingMember", + "two", + "nonExistingMember", + ]), + ).toEqual([1.0, null, 2.0, null]); + expect(await client.zmscore(nonExistingKey, ["one"])).toEqual([ + null, + ]); + + // invalid arg - member list must not be empty + await expect(client.zmscore(key1, [])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a sorted set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.zmscore(stringKey, ["one"]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zcount test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index da4233ae7b..f5eaf25477 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -561,6 +561,8 @@ export async function transactionTest( args.push({ three: 3.5 }); baseTransaction.zdiffstore(key13, [key13, key13]); args.push(0); + baseTransaction.zmscore(key12, ["two", "one"]); + args.push([2.0, 1.0]); } baseTransaction.zinterstore(key12, [key12, key13]); From 9c711ed0bbd709b8c59e69214d911a173b080d5c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 19 Jul 2024 18:52:33 -0700 Subject: [PATCH 028/236] Node: Add `BITCOUNT` commnd. (#1982) * Add `BITCOUNT` commnd. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 10 +++ node/src/BaseClient.ts | 28 ++++++++- node/src/Commands.ts | 13 ++++ node/src/Transaction.ts | 19 ++++++ node/src/commands/BitOffsetOptions.ts | 60 ++++++++++++++++++ node/tests/SharedTests.ts | 88 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 20 +++++- 8 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 node/src/commands/BitOffsetOptions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 10456b9734..2820b2c60a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index d3a2d6af84..513e6198f5 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -74,6 +74,11 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { + BitOffsetOptions, + BitmapIndexType, + ConditionalChange, + GeoAddOptions, + GeospatialData, GlideClient, GlideClusterClient, GlideClientConfiguration, @@ -121,6 +126,11 @@ function initialize() { } = nativeBinding; module.exports = { + BitOffsetOptions, + BitmapIndexType, + ConditionalChange, + GeoAddOptions, + GeospatialData, GlideClient, GlideClusterClient, GlideClientConfiguration, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 2b8e3f5733..fe7f2656ef 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -10,7 +10,6 @@ import { } from "glide-rs"; import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; -import { LPosOptions } from "./commands/LPosOptions"; import { AggregationType, ExpireOptions, @@ -27,6 +26,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createDecr, createDecrBy, createDel, @@ -124,6 +124,8 @@ import { createZRevRankWithScore, createZScore, } from "./Commands"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; +import { LPosOptions } from "./commands/LPosOptions"; import { ClosingError, ConfigurationError, @@ -3337,6 +3339,30 @@ export class BaseClient { return this.createWritePromise(createLPos(key, element, options)); } + /** + * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can + * optionally be provided to count the number of bits in a specific string interval. + * + * See https://valkey.io/commands/bitcount for more details. + * + * @param key - The key for the string to count the set bits of. + * @param options - The offset options. + * @returns If `options` is provided, returns the number of set bits in the string interval specified by `options`. + * If `options` is not provided, returns the number of set bits in the string stored at `key`. + * Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + * + * @example + * ```typescript + * console.log(await client.bitcount("my_key1")); // Output: 2 - The string stored at "my_key1" contains 2 set bits. + * console.log(await client.bitcount("my_key2", OffsetOptions(1, 3))); // Output: 2 - The second to fourth bytes of the string stored at "my_key2" contain 2 set bits. + * console.log(await client.bitcount("my_key3", OffsetOptions(1, 1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the second bit of the string stored at "my_key3" is set. + * console.log(await client.bitcount("my_key3", OffsetOptions(-1, -1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the last bit of the string stored at "my_key3" is set. + * ``` + */ + public bitcount(key: string, options?: BitOffsetOptions): Promise { + return this.createWritePromise(createBitCount(key, options)); + } + /** * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 190f39b1bd..4525f39320 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -8,6 +8,7 @@ import { LPosOptions } from "./commands/LPosOptions"; import { FlushMode } from "./commands/FlushMode"; import { command_request } from "./ProtobufMessage"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; @@ -1576,6 +1577,18 @@ export function createFunctionLoad( return createCommand(RequestType.FunctionLoad, args); } +/** + * @internal + */ +export function createBitCount( + key: string, + options?: BitOffsetOptions, +): command_request.Command { + const args = [key]; + if (options) args.push(...options.toArgs()); + return createCommand(RequestType.BitCount, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 63eeecf6e8..7dd14e0aec 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -20,6 +20,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createClientGetName, createClientId, createConfigGet, @@ -137,6 +138,7 @@ import { } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { FlushMode } from "./commands/FlushMode"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { LPosOptions } from "./commands/LPosOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; @@ -1925,6 +1927,23 @@ export class BaseTransaction> { return this.addAndReturn(createDBSize()); } + /** + * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can + * optionally be provided to count the number of bits in a specific string interval. + * + * See https://valkey.io/commands/bitcount for more details. + * + * @param key - The key for the string to count the set bits of. + * @param options - The offset options. + * + * Command Response - If `options` is provided, returns the number of set bits in the string interval specified by `options`. + * If `options` is not provided, returns the number of set bits in the string stored at `key`. + * Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + */ + public bitcount(key: string, options?: BitOffsetOptions): T { + return this.addAndReturn(createBitCount(key, options)); + } + /** * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. diff --git a/node/src/commands/BitOffsetOptions.ts b/node/src/commands/BitOffsetOptions.ts new file mode 100644 index 0000000000..64f6f8a82e --- /dev/null +++ b/node/src/commands/BitOffsetOptions.ts @@ -0,0 +1,60 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +// Import below added to fix up the TSdoc link, but eslint blames for unused import. +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { BaseClient } from "src/BaseClient"; + +/** + * Enumeration specifying if index arguments are BYTE indexes or BIT indexes. + * Can be specified in {@link BitOffsetOptions}, which is an optional argument to the {@link BaseClient.bitcount|bitcount} command. + * + * since - Valkey version 7.0.0. + */ +export enum BitmapIndexType { + /** Specifies that indexes provided to {@link BitOffsetOptions} are byte indexes. */ + BYTE = "BYTE", + /** Specifies that indexes provided to {@link BitOffsetOptions} are bit indexes. */ + BIT = "BIT", +} + +/** + * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are + * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. + * The offsets can also be negative numbers indicating offsets starting at the end of the string, with `-1` being + * the last index of the string, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitcount/ for more details. + */ +export class BitOffsetOptions { + private start: number; + private end: number; + private indexType?: BitmapIndexType; + + /** + * @param start - The starting offset index. + * @param end - The ending offset index. + * @param indexType - The index offset type. This option can only be specified if you are using server version 7.0.0 or above. + * Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. + * If no index type is provided, the indexes will be assumed to be byte indexes. + */ + constructor(start: number, end: number, indexType?: BitmapIndexType) { + this.start = start; + this.end = end; + this.indexType = indexType; + } + + /** + * Converts BitOffsetOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args = [this.start.toString(), this.end.toString()]; + + if (this.indexType) args.push(this.indexType); + + return args; + } +} diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 40c4636f74..969d187c3c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -27,6 +27,10 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/commands/BitOffsetOptions"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; @@ -4317,6 +4321,90 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const value = "foobar"; + + checkSimple(await client.set(key1, value)).toEqual("OK"); + expect(await client.bitcount(key1)).toEqual(26); + expect( + await client.bitcount(key1, new BitOffsetOptions(1, 1)), + ).toEqual(6); + expect( + await client.bitcount(key1, new BitOffsetOptions(0, -5)), + ).toEqual(10); + // non-existing key + expect(await client.bitcount(uuidv4())).toEqual(0); + expect( + await client.bitcount( + uuidv4(), + new BitOffsetOptions(5, 30), + ), + ).toEqual(0); + // key exists, but it is not a string + expect(await client.sadd(key2, [value])).toEqual(1); + await expect(client.bitcount(key2)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitcount(key2, new BitOffsetOptions(1, 1)), + ).rejects.toThrow(RequestError); + + if (await checkIfServerVersionLessThan("7.0.0")) { + await expect( + client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BIT), + ), + ).rejects.toThrow(); + await expect( + client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).rejects.toThrow(); + } else { + expect( + await client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).toEqual(16); + expect( + await client.bitcount( + key1, + new BitOffsetOptions(5, 30, BitmapIndexType.BIT), + ), + ).toEqual(17); + expect( + await client.bitcount( + key1, + new BitOffsetOptions(5, -5, BitmapIndexType.BIT), + ), + ).toEqual(23); + expect( + await client.bitcount( + uuidv4(), + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).toEqual(0); + // key exists, but it is not a string + await expect( + client.bitcount( + key2, + new BitOffsetOptions(1, 1, BitmapIndexType.BYTE), + ), + ).rejects.toThrow(RequestError); + } + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `geoadd test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index f5eaf25477..3ed904aff1 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -18,10 +18,14 @@ import { ReturnType, Transaction, } from ".."; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/commands/BitOffsetOptions"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; -import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { checkIfServerVersionLessThan } from "./SharedTests"; +import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; beforeAll(() => { Logger.init("info"); @@ -629,6 +633,20 @@ export async function transactionTest( baseTransaction.setbit(key17, 1, 1); args.push(0); + baseTransaction.set(key17, "foobar"); + args.push("OK"); + baseTransaction.bitcount(key17); + args.push(26); + baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); + args.push(6); + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.bitcount( + key17, + new BitOffsetOptions(5, 30, BitmapIndexType.BIT), + ); + args.push(17); + } baseTransaction.pfadd(key11, ["a", "b", "c"]); args.push(1); From 83cec49e893a5955e1d4970aa9b53e8d79478aca Mon Sep 17 00:00:00 2001 From: Bar Shaul <88437685+barshaul@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:26:34 +0300 Subject: [PATCH 029/236] Fixed Java ORT not to include irrelevant dependencies (#1975) --- java/.ort.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/java/.ort.yml b/java/.ort.yml index fa5555e99e..ace1740f01 100644 --- a/java/.ort.yml +++ b/java/.ort.yml @@ -11,10 +11,13 @@ excludes: scopes: - pattern: "test.*" reason: "TEST_DEPENDENCY_OF" - comment: Packages for testing only. Not part of released artifacts." + comment: Packages for testing only. Not part of released artifacts. - pattern: "(spotbugs.*|spotbugsSlf4j.*)" reason: "TEST_DEPENDENCY_OF" - comment: Packages for static analysis only. Not part of released artifacts." + comment: Packages for static analysis only. Not part of released artifacts. - pattern: "jacoco.*" reason: "TEST_DEPENDENCY_OF" - comment: Packages for code coverage verification only. Not part of released artifacts." + comment: Packages for code coverage verification only. Not part of released artifacts. + - pattern: "compileClasspath.*" + reason: "TEST_DEPENDENCY_OF" + comment: Packages for Gradle only. Not part of released artifacts. From 1e678b8ebd34f21b2b82b704329564e796f501b0 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Mon, 22 Jul 2024 14:23:00 -0700 Subject: [PATCH 030/236] Node: add GETBIT command (#1989) Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 22 ++++++++++++++++++++++ node/src/Commands.ts | 10 ++++++++++ node/src/Transaction.ts | 17 +++++++++++++++++ node/tests/SharedTests.ts | 37 ++++++++++++++++++++++++++++++++++--- node/tests/TestUtilities.ts | 2 ++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2820b2c60a..85833e72fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added GETBIT command ([#1989](https://github.com/valkey-io/valkey-glide/pull/1989)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) * 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index fe7f2656ef..3464809038 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -35,6 +35,7 @@ import { createExpireAt, createGeoAdd, createGet, + createGetBit, createGetDel, createHDel, createHExists, @@ -972,6 +973,27 @@ export class BaseClient { return this.createWritePromise(createDecrBy(key, amount)); } + /** + * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal + * to zero. + * + * See https://valkey.io/commands/getbit/ for more details. + * + * @param key - The key of the string. + * @param offset - The index of the bit to return. + * @returns The bit at the given `offset` of the string. Returns `0` if the key is empty or if the `offset` exceeds + * the length of the string. + * + * @example + * ```typescript + * const result = await client.getbit("key", 1); + * console.log(result); // Output: 1 - The second bit of the string stored at "key" is set to 1. + * ``` + */ + public getbit(key: string, offset: number): Promise { + return this.createWritePromise(createGetBit(key, offset)); + } + /** * Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, with * `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less than diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 4525f39320..e442ee1f80 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -446,6 +446,16 @@ export function createDecrBy( return createCommand(RequestType.DecrBy, [key, amount.toString()]); } +/** + * @internal + */ +export function createGetBit( + key: string, + offset: number, +): command_request.Command { + return createCommand(RequestType.GetBit, [key, offset.toString()]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7dd14e0aec..d11e25cbcb 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -42,6 +42,7 @@ import { createFunctionLoad, createGeoAdd, createGet, + createGetBit, createGetDel, createHDel, createHExists, @@ -387,6 +388,22 @@ export class BaseTransaction> { return this.addAndReturn(createDecrBy(key, amount)); } + /** + * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal + * to zero. + * + * See https://valkey.io/commands/getbit/ for more details. + * + * @param key - The key of the string. + * @param offset - The index of the bit to return. + * + * Command Response - The bit at the given `offset` of the string. Returns `0` if the key is empty or if the + * `offset` exceeds the length of the string. + */ + public getbit(key: string, offset: number): T { + return this.addAndReturn(createGetBit(key, offset)); + } + /** * Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, with * `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less than diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 969d187c3c..bab16def9a 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -495,12 +495,43 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `getbit test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; + + expect(await client.set(key, "foo")).toEqual("OK"); + expect(await client.getbit(key, 1)).toEqual(1); + // When offset is beyond the string length, the string is assumed to be a contiguous space with 0 bits. + expect(await client.getbit(key, 1000)).toEqual(0); + // When key does not exist it is assumed to be an empty string, so offset is always out of range and the + // value is also assumed to be a contiguous space with 0 bits. + expect(await client.getbit(nonExistingKey, 1)).toEqual(0); + + // invalid argument - offset can't be negative + await expect(client.getbit(key, -1)).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a string + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + await expect(client.getbit(setKey, 0)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `setbit test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key = `{key}-${uuidv4()}`; - const stringKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; expect(await client.setbit(key, 1, 1)).toEqual(0); expect(await client.setbit(key, 1, 0)).toEqual(1); @@ -516,8 +547,8 @@ export function runBaseTests(config: { ); // key exists, but it is not a string - expect(await client.sadd(stringKey, ["foo"])).toEqual(1); - await expect(client.setbit(stringKey, 0, 0)).rejects.toThrow( + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + await expect(client.setbit(setKey, 0, 0)).rejects.toThrow( RequestError, ); }, protocol); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 3ed904aff1..d24dfaba20 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -633,6 +633,8 @@ export async function transactionTest( baseTransaction.setbit(key17, 1, 1); args.push(0); + baseTransaction.getbit(key17, 1); + args.push(1); baseTransaction.set(key17, "foobar"); args.push("OK"); baseTransaction.bitcount(key17); From db92a9396f8a6bf7a0b1392121d2a6fd929ea5b3 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Mon, 22 Jul 2024 14:31:45 -0700 Subject: [PATCH 031/236] Node: add `FUNCTION DELETE` command (#1990) * Node: add FUNCTION DELETE command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/Commands.ts | 13 +++- node/src/GlideClient.ts | 21 ++++++ node/src/GlideClusterClient.ts | 29 +++++++++ node/src/Transaction.ts | 20 +++++- node/tests/RedisClient.test.ts | 55 ++++++++++++++++ node/tests/RedisClusterClient.test.ts | 92 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 4 +- 8 files changed, 230 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85833e72fa..1146b4a2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * 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)) * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) +* Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990)) * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) ## 1.0.0 (2024-07-09) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e442ee1f80..2743d95918 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -4,13 +4,13 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; -import { LPosOptions } from "./commands/LPosOptions"; import { FlushMode } from "./commands/FlushMode"; +import { LPosOptions } from "./commands/LPosOptions"; import { command_request } from "./ProtobufMessage"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; -import { GeospatialData } from "./commands/geospatial/GeospatialData"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; +import { GeospatialData } from "./commands/geospatial/GeospatialData"; import RequestType = command_request.RequestType; @@ -1576,6 +1576,15 @@ export function createBLPop( return createCommand(RequestType.BLPop, args); } +/** + * @internal + */ +export function createFunctionDelete( + libraryCode: string, +): command_request.Command { + return createCommand(RequestType.FunctionDelete, [libraryCode]); +} + /** * @internal */ diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 614ea9f260..b89e39a6c4 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -23,6 +23,7 @@ import { createEcho, createFlushAll, createFlushDB, + createFunctionDelete, createFunctionFlush, createFunctionLoad, createInfo, @@ -388,6 +389,26 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createLolwut(options)); } + /** + * Deletes a library and all its functions. + * + * See https://valkey.io/commands/function-delete/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The library name to delete. + * @returns A simple OK response. + * + * @example + * ```typescript + * const result = await client.functionDelete("libName"); + * console.log(result); // Output: 'OK' + * ``` + */ + public functionDelete(libraryCode: string): Promise { + return this.createWritePromise(createFunctionDelete(libraryCode)); + } + /** * Loads a library to Valkey. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d583fd4c89..d552469812 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -23,6 +23,7 @@ import { createEcho, createFlushAll, createFlushDB, + createFunctionDelete, createFunctionFlush, createFunctionLoad, createInfo, @@ -658,6 +659,34 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Deletes a library and all its functions. + * + * See https://valkey.io/commands/function-delete/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The library name to delete. + * @param route - The command will be routed to all primary node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns A simple OK response. + * + * @example + * ```typescript + * const result = await client.functionDelete("libName"); + * console.log(result); // Output: 'OK' + * ``` + */ + public functionDelete( + libraryCode: string, + route?: Routes, + ): Promise { + return this.createWritePromise( + createFunctionDelete(libraryCode), + toProtobufRoute(route), + ); + } + /** * Loads a library to Valkey. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index d11e25cbcb..02e859d840 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -38,6 +38,7 @@ import { createExpireAt, createFlushAll, createFlushDB, + createFunctionDelete, createFunctionFlush, createFunctionLoad, createGeoAdd, @@ -133,13 +134,13 @@ import { createZRem, createZRemRangeByRank, createZRemRangeByScore, - createZScore, createZRevRank, createZRevRankWithScore, + createZScore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; -import { FlushMode } from "./commands/FlushMode"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; +import { FlushMode } from "./commands/FlushMode"; import { LPosOptions } from "./commands/LPosOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; @@ -1856,6 +1857,21 @@ export class BaseTransaction> { return this.addAndReturn(createLolwut(options)); } + /** + * Deletes a library and all its functions. + * + * See https://valkey.io/commands/function-delete/ for details. + * + * since Valkey version 7.0.0. + * + * @param libraryCode - The library name to delete. + * + * Command Response - `OK`. + */ + public functionDelete(libraryCode: string): T { + return this.addAndReturn(createFunctionDelete(libraryCode)); + } + /** * Loads a library to Valkey. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 37c4f5c797..853a8aa769 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -525,6 +525,61 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function delete test_%p", + async (protocol) => { + if (await checkIfServerVersionLessThan("7.0.0")) return; + + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + try { + const libName = "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + expect( + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]), + ).toEqual([]); + + checkSimple(await client.functionLoad(code)).toEqual(libName); + + // Delete the function + expect(await client.functionDelete(libName)).toEqual("OK"); + + // TODO use commands instead of customCommand once implemented + // verify function does not exist + expect( + await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]), + ).toEqual([]); + + // deleting a non-existing library + await expect(client.functionDelete(libName)).rejects.toThrow( + `Library not found`, + ); + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP3])("simple pubsub test", async (protocol) => { const pattern = "*"; const channel = "test-channel"; diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index b8b3008ca2..6ca0a3f802 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -797,6 +797,98 @@ describe("GlideClusterClient", () => { }, ); + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function delete", + async () => { + if (await checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + // TODO use commands instead of customCommand once implemented + // verify function does not yet exist + let functionList = await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + checkSimple( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // Delete the function + expect( + await client.functionDelete(libName, route), + ).toEqual("OK"); + + // TODO use commands instead of customCommand once implemented + // verify function does not exist + functionList = await client.customCommand([ + "FUNCTION", + "LIST", + "LIBRARYNAME", + libName, + ]); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Delete a non-existing library + await expect( + client.functionDelete(libName, route), + ).rejects.toThrow(`Library not found`); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + it.each([ [true, ProtocolVersion.RESP3], [false, ProtocolVersion.RESP3], diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d24dfaba20..bb292de4ee 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -23,9 +23,9 @@ import { BitOffsetOptions, } from "../build-ts/src/commands/BitOffsetOptions"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; +import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { checkIfServerVersionLessThan } from "./SharedTests"; -import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; beforeAll(() => { Logger.init("info"); @@ -676,6 +676,8 @@ export async function transactionTest( args.push(libName); baseTransaction.functionLoad(code, true); args.push(libName); + baseTransaction.functionDelete(libName); + args.push("OK"); baseTransaction.functionFlush(); args.push("OK"); baseTransaction.functionFlush(FlushMode.ASYNC); From 3bbe51b841296601ac073294f8e798d919b68cf8 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Mon, 22 Jul 2024 20:27:57 -0700 Subject: [PATCH 032/236] Node: added ZMPOP command (#1994) * Add zmpop command Signed-off-by: Guian Gumpac --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 35 ++++++++++++++ node/src/Commands.ts | 30 ++++++++++++ node/src/Transaction.ts | 25 +++++++++- node/tests/RedisClusterClient.test.ts | 4 ++ node/tests/SharedTests.ts | 66 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 5 ++ 8 files changed, 167 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1146b4a2a1..37a02b88cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) * Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990)) * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) +* Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) ## 1.0.0 (2024-07-09) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 513e6198f5..dce8b31649 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -116,6 +116,7 @@ function initialize() { ClusterTransaction, Transaction, PubSubMsg, + ScoreFilter, createLeakedArray, createLeakedAttribute, createLeakedBigint, @@ -168,6 +169,7 @@ function initialize() { ClusterTransaction, Transaction, PubSubMsg, + ScoreFilter, createLeakedArray, createLeakedAttribute, createLeakedBigint, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 3464809038..b73fa04673 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -19,6 +19,7 @@ import { RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, SetOptions, StreamAddOptions, StreamReadOptions, @@ -112,6 +113,7 @@ import { createZDiffWithScores, createZInterCard, createZInterstore, + createZMPop, createZMScore, createZPopMax, createZPopMin, @@ -3416,6 +3418,39 @@ export class BaseClient { ); } + /** + * Pops a member-score pair from the first non-empty sorted set, with the given `keys` + * being checked in the order they are provided. + * + * See https://valkey.io/commands/zmpop/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets. + * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or + * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. + * @param count - The number of elements to pop. + * @returns A two-element `array` containing the key name of the set from which the element + * was popped, and a member-score `Record` of the popped element. + * If no member could be popped, returns `null`. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); + * await client.zadd("zSet2", { four: 4.0 }); + * console.log(await client.zmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 2)); + * // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1". + * ``` + */ + public zmpop( + key: string[], + modifier: ScoreFilter, + count?: number, + ): Promise<[string, [Record]] | null> { + return this.createWritePromise(createZMPop(key, modifier, count)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 2743d95918..e51036aa24 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1869,3 +1869,33 @@ export function createZRevRankWithScore( ): command_request.Command { return createCommand(RequestType.ZRevRank, [key, member, "WITHSCORE"]); } + +/** + * Mandatory option for zmpop. + * Defines which elements to pop from the sorted set. + */ +export enum ScoreFilter { + /** Pop elements with the highest scores. */ + MAX = "MAX", + /** Pop elements with the lowest scores. */ + MIN = "MIN", +} + +/** + * @internal + */ +export function createZMPop( + keys: string[], + modifier: ScoreFilter, + count?: number, +): command_request.Command { + const args: string[] = [keys.length.toString()].concat(keys); + args.push(modifier); + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.ZMPop, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 02e859d840..3e49e2ab48 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -13,6 +13,7 @@ import { RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, SetOptions, StreamAddOptions, StreamReadOptions, @@ -136,6 +137,7 @@ import { createZRemRangeByScore, createZRevRank, createZRevRankWithScore, + createZMPop, createZScore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1940,7 +1942,7 @@ export class BaseTransaction> { * @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` + * 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. @@ -2001,6 +2003,27 @@ export class BaseTransaction> { createGeoAdd(key, membersToGeospatialData, options), ); } + + /** + * Pops a member-score pair from the first non-empty sorted set, with the given `keys` + * being checked in the order they are provided. + * + * See https://valkey.io/commands/zmpop/ for more details. + * + * @param keys - The keys of the sorted sets. + * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or + * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. + * @param count - The number of elements to pop. + * + * Command Response - A two-element `array` containing the key name of the set from which the + * element was popped, and a member-score `Record` of the popped element. + * If no member could be popped, returns `null`. + * + * since Valkey version 7.0.0. + */ + public zmpop(keys: string[], modifier: ScoreFilter, count?: number): T { + return this.addAndReturn(createZMPop(keys, modifier, count)); + } } /** diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 6ca0a3f802..e039c96ad5 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -19,6 +19,7 @@ import { InfoOptions, ProtocolVersion, Routes, + ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; @@ -323,6 +324,9 @@ describe("GlideClusterClient", () => { if (!versionLessThan7) { promises.push(client.zintercard(["abc", "zxy", "lkn"])); + promises.push( + client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), + ); } for (const promise of promises) { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index bab16def9a..a479013691 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -14,6 +14,7 @@ import { InsertPosition, ProtocolVersion, RequestError, + ScoreFilter, Script, parseInfoResponse, } from "../"; @@ -4545,6 +4546,71 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zmpop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("7.0.0")) return; + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const nonExistingKey = "{key}-0" + uuidv4(); + const stringKey = "{key}-string" + uuidv4(); + + expect(await client.zadd(key1, { a1: 1, b1: 2 })).toEqual(2); + expect(await client.zadd(key2, { a2: 0.1, b2: 0.2 })).toEqual( + 2, + ); + + checkSimple( + await client.zmpop([key1, key2], ScoreFilter.MAX), + ).toEqual([key1, { b1: 2 }]); + checkSimple( + await client.zmpop([key2, key1], ScoreFilter.MAX, 10), + ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); + + expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN)) + .toBeNull; + expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN, 1)) + .toBeNull; + + // key exists, but it is not a sorted set + expect(await client.set(stringKey, "value")).toEqual("OK"); + await expect( + client.zmpop([stringKey], ScoreFilter.MAX), + ).rejects.toThrow(RequestError); + await expect( + client.zmpop([stringKey], ScoreFilter.MAX, 1), + ).rejects.toThrow(RequestError); + + // incorrect argument: key list should not be empty + await expect( + client.zmpop([], ScoreFilter.MAX, 1), + ).rejects.toThrow(RequestError); + + // incorrect argument: count should be greater than 0 + await expect( + client.zmpop([key1], ScoreFilter.MAX, 0), + ).rejects.toThrow(RequestError); + + // check that order of entries in the response is preserved + const entries: Record = {}; + + for (let i = 0; i < 10; i++) { + // a0 => 0, a1 => 1 etc + entries["a" + i] = i; + } + + expect(await client.zadd(key2, entries)).toEqual(10); + const result = await client.zmpop([key2], ScoreFilter.MIN, 10); + + if (result) { + expect(result[1]).toEqual(entries); + } + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index bb292de4ee..28d76d7685 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -16,6 +16,7 @@ import { Logger, ProtocolVersion, ReturnType, + ScoreFilter, Transaction, } from ".."; import { @@ -593,6 +594,10 @@ export async function transactionTest( args.push(0); baseTransaction.zintercard([key8, key14], 1); args.push(0); + baseTransaction.zmpop([key14], ScoreFilter.MAX); + args.push([key14, { two: 2.0 }]); + baseTransaction.zmpop([key14], ScoreFilter.MAX, 1); + args.push([key14, { one: 1.0 }]); } baseTransaction.xadd(key9, [["field", "value1"]], { id: "0-1" }); From 0438afec8b2aba231b07718f2310707ce165092a Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Tue, 23 Jul 2024 09:16:41 -0700 Subject: [PATCH 033/236] Node: fix ZADD bug (#1995) * fixed bug when using only changed option Signed-off-by: Guian Gumpac --- CHANGELOG.md | 3 +++ node/src/BaseClient.ts | 15 ++++----------- node/src/Commands.ts | 14 +++++++++++--- node/src/Transaction.ts | 13 ++----------- node/tests/SharedTests.ts | 28 ++++++++++++---------------- 5 files changed, 32 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a02b88cd..dd10d71dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) +#### Fixes +* Node: Fix ZADD bug where command could not be called with only the `changed` optional parameter ([#1995](https://github.com/valkey-io/valkey-glide/pull/1995)) + ## 1.0.0 (2024-07-09) #### Changes diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b73fa04673..ff714bb793 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2210,21 +2210,20 @@ export class BaseClient { * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. * @param options - The ZAdd options. - * @param changed - Modify the return value from the number of new elements added, to the total number of elements changed. * @returns The number of elements added to the sorted set. * If `changed` is set, returns the number of elements updated in the sorted set. * * @example * ```typescript * // Example usage of the zadd method to add elements to a sorted set - * const result = await client.zadd("my_sorted_set", \{ "member1": 10.5, "member2": 8.2 \}); + * const result = await client.zadd("my_sorted_set", { "member1": 10.5, "member2": 8.2 }); * console.log(result); // Output: 2 - Indicates that two elements have been added to the sorted set "my_sorted_set." * ``` * * @example * ```typescript * // Example usage of the zadd method to update scores in an existing sorted set - * const result = await client.zadd("existing_sorted_set", { member1: 15.0, member2: 5.5 }, options={ conditionalChange: "onlyIfExists" } , changed=true); + * const result = await client.zadd("existing_sorted_set", { member1: 15.0, member2: 5.5 }, { conditionalChange: "onlyIfExists", changed: true }); * console.log(result); // Output: 2 - Updates the scores of two existing members in the sorted set "existing_sorted_set." * ``` */ @@ -2232,15 +2231,9 @@ export class BaseClient { key: string, membersScoresMap: Record, options?: ZAddOptions, - changed?: boolean, ): Promise { return this.createWritePromise( - createZAdd( - key, - membersScoresMap, - options, - changed ? "CH" : undefined, - ), + createZAdd(key, membersScoresMap, options), ); } @@ -2277,7 +2270,7 @@ export class BaseClient { options?: ZAddOptions, ): Promise { return this.createWritePromise( - createZAdd(key, { [member]: increment }, options, "INCR"), + createZAdd(key, { [member]: increment }, options, true), ); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e51036aa24..2e8f7d5eb6 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -938,6 +938,10 @@ export type ZAddOptions = { * is greater than the current score. Equivalent to `GT` in the Redis API. */ updateOptions?: "scoreLessThanCurrent" | "scoreGreaterThanCurrent"; + /** + * Modify the return value from the number of new elements added, to the total number of elements changed. + */ + changed?: boolean; }; /** @@ -947,7 +951,7 @@ export function createZAdd( key: string, membersScoresMap: Record, options?: ZAddOptions, - changedOrIncr?: "CH" | "INCR", + incr: boolean = false, ): command_request.Command { let args = [key]; @@ -969,10 +973,14 @@ export function createZAdd( } else if (options.updateOptions === "scoreGreaterThanCurrent") { args.push("GT"); } + + if (options.changed) { + args.push("CH"); + } } - if (changedOrIncr) { - args.push(changedOrIncr); + if (incr) { + args.push("INCR"); } args = args.concat( diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3e49e2ab48..bc18eedc6f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1129,7 +1129,6 @@ export class BaseTransaction> { * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. * @param options - The ZAdd options. - * @param changed - Modify the return value from the number of new elements added, to the total number of elements changed. * * Command Response - The number of elements added to the sorted set. * If `changed` is set, returns the number of elements updated in the sorted set. @@ -1138,16 +1137,8 @@ export class BaseTransaction> { key: string, membersScoresMap: Record, options?: ZAddOptions, - changed?: boolean, ): T { - return this.addAndReturn( - createZAdd( - key, - membersScoresMap, - options, - changed ? "CH" : undefined, - ), - ); + return this.addAndReturn(createZAdd(key, membersScoresMap, options)); } /** Increments the score of member in the sorted set stored at `key` by `increment`. @@ -1170,7 +1161,7 @@ export class BaseTransaction> { options?: ZAddOptions, ): T { return this.addAndReturn( - createZAdd(key, { [member]: increment }, options, "INCR"), + createZAdd(key, { [member]: increment }, options, true), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index a479013691..e89cebfaae 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2093,9 +2093,13 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const key = uuidv4(); const membersScores = { one: 1, two: 2, three: 3 }; + const newMembersScores = { one: 2, two: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); expect(await client.zaddIncr(key, "one", 2)).toEqual(3.0); + expect( + await client.zadd(key, newMembersScores, { changed: true }), + ).toEqual(2); }, protocol); }, config.timeout, @@ -2146,25 +2150,17 @@ export function runBaseTests(config: { membersScores["one"] = 10; expect( - await client.zadd( - key, - membersScores, - { - updateOptions: "scoreGreaterThanCurrent", - }, - true, - ), + await client.zadd(key, membersScores, { + updateOptions: "scoreGreaterThanCurrent", + changed: true, + }), ).toEqual(1); expect( - await client.zadd( - key, - membersScores, - { - updateOptions: "scoreLessThanCurrent", - }, - true, - ), + await client.zadd(key, membersScores, { + updateOptions: "scoreLessThanCurrent", + changed: true, + }), ).toEqual(0); expect( From 52a7ed6eaf4ca3284a66100239f024d50c006e18 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 23 Jul 2024 09:50:22 -0700 Subject: [PATCH 034/236] Node: Add `GEOPOS` command. (#1991) * Add `GEOPOS` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 38 +++++++++++++++++++++++++++++++++---- node/src/Commands.ts | 11 ++++++++++- node/src/Transaction.ts | 20 ++++++++++++++++++- node/tests/SharedTests.ts | 25 +++++++++++++++++++++++- node/tests/TestUtilities.ts | 5 +++++ 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd10d71dc0..cb5d755dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ff714bb793..390c5ca22e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -35,6 +35,7 @@ import { createExpire, createExpireAt, createGeoAdd, + createGeoPos, createGet, createGetBit, createGetDel, @@ -84,7 +85,6 @@ import { createSCard, createSDiff, createSDiffStore, - createSetBit, createSInter, createSInterCard, createSInterStore, @@ -97,6 +97,7 @@ import { createSUnion, createSUnionStore, createSet, + createSetBit, createStrlen, createTTL, createType, @@ -128,6 +129,8 @@ import { createZScore, } from "./Commands"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; +import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; +import { GeospatialData } from "./commands/geospatial/GeospatialData"; import { LPosOptions } from "./commands/LPosOptions"; import { ClosingError, @@ -146,8 +149,6 @@ import { connection_request, response, } from "./ProtobufMessage"; -import { GeospatialData } from "./commands/geospatial/GeospatialData"; -import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ type PromiseFunction = (value?: any) => void; @@ -3397,7 +3398,7 @@ export class BaseClient { * @example * ```typescript * const options = new GeoAddOptions({updateMode: ConditionalChange.ONLY_IF_EXISTS, changed: true}); - * const num = await client.geoadd("mySortedSet", {"Palermo", new GeospatialData(13.361389, 38.115556)}, options); + * const num = await client.geoadd("mySortedSet", new Map([["Palermo", new GeospatialData(13.361389, 38.115556)]]), options); * console.log(num); // Output: 1 - Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated. * ``` */ @@ -3411,6 +3412,35 @@ export class BaseClient { ); } + /** + * Returns the positions (longitude, latitude) of all the specified `members` of the + * geospatial index represented by the sorted set at `key`. + * + * See https://valkey.io/commands/geopos for more details. + * + * @param key - The key of the sorted set. + * @param members - The members for which to get the positions. + * @returns A 2D `Array` which represents positions (longitude and latitude) corresponding to the + * given members. The order of the returned positions matches the order of the input members. + * If a member does not exist, its position will be `null`. + * + * @example + * ```typescript + * const data = new Map([["Palermo", new GeospatialData(13.361389, 38.115556)], ["Catania", new GeospatialData(15.087269, 37.502669)]]); + * await client.geoadd("mySortedSet", data); + * const result = await client.geopos("mySortedSet", ["Palermo", "Catania", "NonExisting"]); + * // When added via GEOADD, the geospatial coordinates are converted into a 52 bit geohash, so the coordinates + * // returned might not be exactly the same as the input values + * console.log(result); // Output: [[13.36138933897018433, 38.11555639549629859], [15.08726745843887329, 37.50266842333162032], null] + * ``` + */ + public geopos( + key: string, + members: string[], + ): Promise<(number[] | null)[]> { + return this.createWritePromise(createGeoPos(key, members)); + } + /** * Pops a member-score pair from the first non-empty sorted set, with the given `keys` * being checked in the order they are provided. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 2e8f7d5eb6..3eeb042ad7 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1854,10 +1854,19 @@ export function createGeoAdd( args = args.concat(coord.toArgs()); args.push(member); }); - return createCommand(RequestType.GeoAdd, args); } +/** + * @internal + */ +export function createGeoPos( + key: string, + members: string[], +): command_request.Command { + return createCommand(RequestType.GeoPos, [key].concat(members)); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index bc18eedc6f..c04bc96817 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -43,6 +43,7 @@ import { createFunctionFlush, createFunctionLoad, createGeoAdd, + createGeoPos, createGet, createGetBit, createGetDel, @@ -126,6 +127,7 @@ import { createZDiffWithScores, createZInterCard, createZInterstore, + createZMPop, createZMScore, createZPopMax, createZPopMin, @@ -137,7 +139,6 @@ import { createZRemRangeByScore, createZRevRank, createZRevRankWithScore, - createZMPop, createZScore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1995,6 +1996,23 @@ export class BaseTransaction> { ); } + /** + * Returns the positions (longitude, latitude) of all the specified `members` of the + * geospatial index represented by the sorted set at `key`. + * + * See https://valkey.io/commands/geopos for more details. + * + * @param key - The key of the sorted set. + * @param members - The members for which to get the positions. + * + * Command Response - A 2D `Array` which represents positions (longitude and latitude) corresponding to the + * given members. The order of the returned positions matches the order of the input members. + * If a member does not exist, its position will be `null`. + */ + public geopos(key: string, members: string[]): T { + return this.addAndReturn(createGeoPos(key, members)); + } + /** * Pops a member-score pair from the first non-empty sorted set, with the given `keys` * being checked in the order they are provided. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index e89cebfaae..a60c789d6c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4434,7 +4434,7 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `geoadd test_%p`, + `geoadd geopos test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); @@ -4452,6 +4452,28 @@ export function runBaseTests(config: { // default geoadd expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); + let geopos = await client.geopos(key1, [ + "Palermo", + "Catania", + "New York", + ]); + // inner array is possibly null, we need a null check or a cast + expect(geopos[0]?.[0]).toBeCloseTo(13.361389, 5); + expect(geopos[0]?.[1]).toBeCloseTo(38.115556, 5); + expect(geopos[1]?.[0]).toBeCloseTo(15.087269, 5); + expect(geopos[1]?.[1]).toBeCloseTo(37.502669, 5); + expect(geopos[2]).toBeNull(); + + // empty array of places + geopos = await client.geopos(key1, []); + expect(geopos).toEqual([]); + + // not existing key + geopos = await client.geopos(key2, []); + expect(geopos).toEqual([]); + geopos = await client.geopos(key2, ["Palermo"]); + expect(geopos).toEqual([null]); + // with update mode options membersToCoordinates.set( "Catania", @@ -4499,6 +4521,7 @@ export function runBaseTests(config: { await expect( client.geoadd(key2, membersToCoordinates), ).rejects.toThrow(); + await expect(client.geopos(key2, ["*_*"])).rejects.toThrow(); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 28d76d7685..bb34980827 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -667,6 +667,11 @@ export async function transactionTest( ]), ); args.push(2); + baseTransaction.geopos(key18, ["Palermo", "Catania"]); + args.push([ + [13.36138933897018433, 38.11555639549629859], + [15.08726745843887329, 37.50266842333162032], + ]); const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); From 7ca26232f4e6965e89b45c6c83db01c45aa5f91a Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:19:37 -0700 Subject: [PATCH 035/236] Node: Add command GEODIST (#1988) * Node: Add command GEODIST --------- Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 ++ node/src/BaseClient.ts | 31 ++++++++++++++++++++ node/src/Commands.ts | 35 +++++++++++++++++++++++ node/src/Transaction.ts | 24 ++++++++++++++++ node/tests/SharedTests.ts | 56 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 5 ++++ 7 files changed, 154 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5d755dc6..22c596d983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ * Node: Added DBSize command ([#1932](https://github.com/valkey-io/valkey-glide/pull/1932)) * Node: Added GeoAdd command ([#1980](https://github.com/valkey-io/valkey-glide/pull/1980)) * Node: Added ZRevRank command ([#1977](https://github.com/valkey-io/valkey-glide/pull/1977)) +* Node: Added GeoDist command ([#1988](https://github.com/valkey-io/valkey-glide/pull/1988)) #### 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/npm/glide/index.ts b/node/npm/glide/index.ts index dce8b31649..d4b45fe370 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -93,6 +93,7 @@ function initialize() { LPosOptions, ExpireOptions, FlushMode, + GeoUnit, InfoOptions, InsertPosition, SetOptions, @@ -146,6 +147,7 @@ function initialize() { LPosOptions, ExpireOptions, FlushMode, + GeoUnit, InfoOptions, InsertPosition, SetOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 390c5ca22e..54ff383b2f 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -35,6 +35,7 @@ import { createExpire, createExpireAt, createGeoAdd, + createGeoDist, createGeoPos, createGet, createGetBit, @@ -127,6 +128,7 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + GeoUnit, } from "./Commands"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; @@ -3474,6 +3476,35 @@ export class BaseClient { return this.createWritePromise(createZMPop(key, modifier, count)); } + /** + * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. + * + * See https://valkey.io/commands/geodist/ for more details. + * + * @param key - The key of the sorted set. + * @param member1 - The name of the first member. + * @param member2 - The name of the second member. + * @param geoUnit - The unit of distance measurement - see {@link GeoUnit}. If not specified, the default unit is {@link GeoUnit.METERS}. + * @returns The distance between `member1` and `member2`. Returns `null`, if one or both members do not exist, + * or if the key does not exist. + * + * @example + * ```typescript + * const result = await client.geodist("mySortedSet", "Place1", "Place2", GeoUnit.KILOMETERS); + * console.log(num); // Output: the distance between Place1 and Place2. + * ``` + */ + public geodist( + key: string, + member1: string, + member2: string, + geoUnit?: GeoUnit, + ): Promise { + return this.createWritePromise( + createGeoDist(key, member1, member2, geoUnit), + ); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 3eeb042ad7..72546bab0a 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1857,6 +1857,23 @@ export function createGeoAdd( return createCommand(RequestType.GeoAdd, args); } +/** + * Enumeration representing distance units options for the {@link geodist} command. + */ +export enum GeoUnit { + /** Represents distance in meters. */ + METERS = "m", + + /** Represents distance in kilometers. */ + KILOMETERS = "km", + + /** Represents distance in miles. */ + MILES = "mi", + + /** Represents distance in feet. */ + FEET = "ft", +} + /** * @internal */ @@ -1867,6 +1884,24 @@ export function createGeoPos( return createCommand(RequestType.GeoPos, [key].concat(members)); } +/** + * @internal + */ +export function createGeoDist( + key: string, + member1: string, + member2: string, + geoUnit?: GeoUnit, +): command_request.Command { + const args: string[] = [key, member1, member2]; + + if (geoUnit) { + args.push(geoUnit); + } + + return createCommand(RequestType.GeoDist, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c04bc96817..30f42e49ee 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -43,6 +43,7 @@ import { createFunctionFlush, createFunctionLoad, createGeoAdd, + createGeoDist, createGeoPos, createGet, createGetBit, @@ -140,6 +141,7 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + GeoUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; @@ -2033,6 +2035,28 @@ export class BaseTransaction> { public zmpop(keys: string[], modifier: ScoreFilter, count?: number): T { return this.addAndReturn(createZMPop(keys, modifier, count)); } + + /** + * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. + * + * See https://valkey.io/commands/geodist/ for more details. + * + * @param key - The key of the sorted set. + * @param member1 - The name of the first member. + * @param member2 - The name of the second member. + * @param geoUnit - The unit of distance measurement - see {@link GeoUnit}. If not specified, the default unit is {@link GeoUnit.METERS}. + * + * Command Response - The distance between `member1` and `member2`. Returns `null`, if one or both members do not exist, + * or if the key does not exist. + */ + public geodist( + key: string, + member1: string, + member2: string, + geoUnit?: GeoUnit, + ): T { + return this.addAndReturn(createGeoDist(key, member1, member2, geoUnit)); + } } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index a60c789d6c..f6f4c6ad9e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -17,6 +17,7 @@ import { ScoreFilter, Script, parseInfoResponse, + GeoUnit, } from "../"; import { Client, @@ -4630,6 +4631,61 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geodist test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const member1 = "Palermo"; + const member2 = "Catania"; + const nonExistingMember = "NonExisting"; + const expected = 166274.1516; + const expectedKM = 166.2742; + const delta = 1e-9; + + // adding the geo locations + const membersToCoordinates = new Map(); + membersToCoordinates.set( + member1, + new GeospatialData(13.361389, 38.115556), + ); + membersToCoordinates.set( + member2, + new GeospatialData(15.087269, 37.502669), + ); + expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); + + // checking result with default metric + expect( + await client.geodist(key1, member1, member2), + ).toBeCloseTo(expected, delta); + + // checking result with metric specification of kilometers + expect( + await client.geodist( + key1, + member1, + member2, + GeoUnit.KILOMETERS, + ), + ).toBeCloseTo(expectedKM, delta); + + // null result when member index is missing + expect( + await client.geodist(key1, member1, nonExistingMember), + ).toBeNull(); + + // key exists but holds non-ZSET value + expect(await client.set(key2, "geodist")).toBe("OK"); + await expect( + client.geodist(key2, member1, member2), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index bb34980827..c23fa5a485 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,7 @@ import { BaseClient, BaseClientConfiguration, ClusterTransaction, + GeoUnit, GlideClient, GlideClusterClient, InsertPosition, @@ -672,6 +673,10 @@ export async function transactionTest( [13.36138933897018433, 38.11555639549629859], [15.08726745843887329, 37.50266842333162032], ]); + baseTransaction.geodist(key18, "Palermo", "Catania"); + args.push(166274.1516); + baseTransaction.geodist(key18, "Palermo", "Catania", GeoUnit.KILOMETERS); + args.push(166.2742); const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); From 861019cba507c0ec7c696c3cca0e191f995be6ee Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Tue, 23 Jul 2024 13:28:03 -0700 Subject: [PATCH 036/236] Java: `XRange`/`XRevRange` should return `null` instead of `GlideException` when given a negative count (#1920) * Java: Fix xrange/xrevrange --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + java/client/src/main/java/glide/api/BaseClient.java | 12 ++++++++---- .../src/test/java/glide/SharedCommandTests.java | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c596d983..f590d24c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ #### Fixes * Node: Fix ZADD bug where command could not be called with only the `changed` optional parameter ([#1995](https://github.com/valkey-io/valkey-glide/pull/1995)) +* Java: `XRange`/`XRevRange` should return `null` instead of `GlideException` when given a negative count ([#1920](https://github.com/valkey-io/valkey-glide/pull/1920)) ## 1.0.0 (2024-07-09) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index fceaf82634..1224763f06 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2706,7 +2706,9 @@ public CompletableFuture> xrange( @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(start, end, count), key); return commandManager.submitNewCommand( - XRange, arguments, response -> castMapOf2DArray(handleMapResponse(response), String.class)); + XRange, + arguments, + response -> castMapOf2DArray(handleMapOrNullResponse(response), String.class)); } @Override @@ -2719,7 +2721,8 @@ public CompletableFuture> xrange( return commandManager.submitNewCommand( XRange, arguments, - response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + response -> + castMapOf2DArray(handleBinaryStringMapOrNullResponse(response), GlideString.class)); } @Override @@ -2752,7 +2755,7 @@ public CompletableFuture> xrevrange( return commandManager.submitNewCommand( XRevRange, arguments, - response -> castMapOf2DArray(handleMapResponse(response), String.class)); + response -> castMapOf2DArray(handleMapOrNullResponse(response), String.class)); } @Override @@ -2765,7 +2768,8 @@ public CompletableFuture> xrevrange( return commandManager.submitNewCommand( XRevRange, arguments, - response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + response -> + castMapOf2DArray(handleBinaryStringMapOrNullResponse(response), GlideString.class)); } @Override diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 3a93cb7939..439fa39ff1 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -6292,6 +6292,12 @@ public void xrange_and_xrevrange(BaseClient client) { assertEquals(1, newRevResult.size()); assertNotNull(newRevResult.get(streamId3)); + // xrange, xrevrange should return null with a zero/negative count + assertNull(client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX, 0L).get()); + assertNull(client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN, 0L).get()); + assertNull(client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX, -5L).get()); + assertNull(client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN, -1L).get()); + // xrange against an emptied stream assertEquals(3, client.xdel(key, new String[] {streamId1, streamId2, streamId3}).get()); Map emptiedResult = From 0e8cf5b2c686527b7b44f6cca16ba33ebe09a334 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:06:40 -0700 Subject: [PATCH 037/236] Node: Add command GEOHASH (#1997) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 25 ++++++++++++++++++++++++ node/src/Commands.ts | 11 +++++++++++ node/src/Transaction.ts | 16 ++++++++++++++++ node/tests/SharedTests.ts | 38 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 93 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f590d24c67..ff4afea615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ * Node: Added GeoAdd command ([#1980](https://github.com/valkey-io/valkey-glide/pull/1980)) * Node: Added ZRevRank command ([#1977](https://github.com/valkey-io/valkey-glide/pull/1977)) * Node: Added GeoDist command ([#1988](https://github.com/valkey-io/valkey-glide/pull/1988)) +* Node: Added GeoHash command ([#1997](https://github.com/valkey-io/valkey-glide/pull/1997)) #### 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/BaseClient.ts b/node/src/BaseClient.ts index 54ff383b2f..9119493f3c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -37,6 +37,7 @@ import { createGeoAdd, createGeoDist, createGeoPos, + createGeoHash, createGet, createGetBit, createGetDel, @@ -3505,6 +3506,30 @@ export class BaseClient { ); } + /** + * Returns the `GeoHash` strings representing the positions of all the specified `members` in the sorted set stored at `key`. + * + * See https://valkey.io/commands/geohash/ for more details. + * + * @param key - The key of the sorted set. + * @param members - The array of members whose GeoHash strings are to be retrieved. + * @returns An array of `GeoHash` strings representing the positions of the specified members stored at `key`. + * If a member does not exist in the sorted set, a `null` value is returned for that member. + * + * @example + * ```typescript + * const result = await client.geohash("mySortedSet",["Palermo", "Catania", "NonExisting"]); + * console.log(num); // Output: ["sqc8b49rny0", "sqdtr74hyu0", null] + * ``` + */ + public geohash(key: string, members: string[]): Promise<(string | null)[]> { + return this.createWritePromise<(string | null)[]>( + createGeoHash(key, members), + ).then((hashes) => + hashes.map((hash) => (hash === null ? null : "" + hash)), + ); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 72546bab0a..b34e7be2b1 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1902,6 +1902,17 @@ export function createGeoDist( return createCommand(RequestType.GeoDist, args); } +/** + * @internal + */ +export function createGeoHash( + key: string, + members: string[], +): command_request.Command { + const args: string[] = [key].concat(members); + return createCommand(RequestType.GeoHash, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 30f42e49ee..8a9b1b61e0 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -44,6 +44,7 @@ import { createFunctionLoad, createGeoAdd, createGeoDist, + createGeoHash, createGeoPos, createGet, createGetBit, @@ -2057,6 +2058,21 @@ export class BaseTransaction> { ): T { return this.addAndReturn(createGeoDist(key, member1, member2, geoUnit)); } + + /** + * Returns the `GeoHash` strings representing the positions of all the specified `members` in the sorted set stored at `key`. + * + * See https://valkey.io/commands/geohash/ for more details. + * + * @param key - The key of the sorted set. + * @param members - The array of members whose GeoHash strings are to be retrieved. + * + * Command Response - An array of `GeoHash` strings representing the positions of the specified members stored at `key`. + * If a member does not exist in the sorted set, a `null` value is returned for that member. + */ + public geohash(key: string, members: string[]): T { + return this.addAndReturn(createGeoHash(key, members)); + } } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index f6f4c6ad9e..cbcbbb6834 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4686,6 +4686,44 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geohash test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const members = ["Palermo", "Catania", "NonExisting"]; + const empty: string[] = []; + const expected = ["sqc8b49rny0", "sqdtr74hyu0", null]; + + // adding the geo locations + const membersToCoordinates = new Map(); + membersToCoordinates.set( + "Palermo", + new GeospatialData(13.361389, 38.115556), + ); + membersToCoordinates.set( + "Catania", + new GeospatialData(15.087269, 37.502669), + ); + expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); + + // checking result with default metric + expect(await client.geohash(key1, members)).toEqual(expected); + + // empty members array + expect(await (await client.geohash(key1, empty)).length).toBe( + 0, + ); + + // key exists but holds non-ZSET value + expect(await client.set(key2, "geohash")).toBe("OK"); + await expect(client.geohash(key2, members)).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index c23fa5a485..4a009cef85 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -677,6 +677,8 @@ export async function transactionTest( args.push(166274.1516); baseTransaction.geodist(key18, "Palermo", "Catania", GeoUnit.KILOMETERS); args.push(166.2742); + baseTransaction.geohash(key18, ["Palermo", "Catania", "NonExisting"]); + args.push(["sqc8b49rny0", "sqdtr74hyu0", null]); const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); From 4c93e26966d4877db7941622ff6a7421011b95d8 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Tue, 23 Jul 2024 17:39:16 -0700 Subject: [PATCH 038/236] Update sintercard and xadd docs (#2004) * Update sintercard and xadd docs Signed-off-by: Andrew Carbonetto --- node/src/BaseClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9119493f3c..8adeaa8552 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1734,6 +1734,7 @@ export class BaseClient { * * @remarks When in cluster mode, all `keys` must map to the same hash slot. * @param keys - The keys of the sets. + * @param limit - The limit for the intersection cardinality value. If not specified, or set to `0`, no limit is used. * @returns The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. * * since Valkey version 7.0.0. @@ -2965,6 +2966,7 @@ export class BaseClient { * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. + * @param options - options detailing how to add to the stream. * @returns The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. */ public xadd( From be0363f3cbea0c7ac11596081e75b947697591ac Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:39:19 -0700 Subject: [PATCH 039/236] Java: Add overloads for XADD to allow duplicate entry keys (#1970) * Add overloads for XADD to allow duplicate entry keys Signed-off-by: Jonathan Louie * Change xadd overloads to use nested arrays Signed-off-by: Jonathan Louie * Fix failing tests Signed-off-by: Jonathan Louie * Update CHANGELOG.md with XADD fix Signed-off-by: Jonathan Louie * Update UT to provide better coverage and fix examples Signed-off-by: Jonathan Louie * Use assertDeepEquals in integ test Signed-off-by: Jonathan Louie * Remove usages of sid from XADD examples Signed-off-by: Jonathan Louie * Update documentation for XADD commands Signed-off-by: Jonathan Louie * Update TransactionTestUtilities for XADD with minor PR suggestion Signed-off-by: Jonathan Louie * Add more integ tests to cover other XADD signatures Signed-off-by: Jonathan Louie * Re-add Fixes section to CHANGELOG and move XADD fix there Signed-off-by: Jonathan Louie * Fix XADD tests Signed-off-by: Jonathan Louie * Apply Spotless Signed-off-by: Jonathan Louie --------- Signed-off-by: Jonathan Louie --- CHANGELOG.md | 1 + .../src/main/java/glide/api/BaseClient.java | 38 ++++++ .../api/commands/StreamBaseCommands.java | 103 ++++++++++++++-- .../glide/api/models/BaseTransaction.java | 51 +++++++- .../java/glide/utils/ArrayTransformUtils.java | 57 +++++++++ .../test/java/glide/api/GlideClientTest.java | 112 ++++++++++++++++++ .../glide/api/models/TransactionTests.java | 9 ++ .../test/java/glide/SharedCommandTests.java | 106 +++++++++++++++++ .../java/glide/TransactionTestUtilities.java | 7 ++ 9 files changed, 474 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4afea615..19fed2211b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) #### Fixes +* Java: Add overloads for XADD to allow duplicate entry keys ([#1970](https://github.com/valkey-io/valkey-glide/pull/1970)) * Node: Fix ZADD bug where command could not be called with only the `changed` optional parameter ([#1995](https://github.com/valkey-io/valkey-glide/pull/1995)) * Java: `XRange`/`XRevRange` should return `null` instead of `GlideException` when given a negative count ([#1920](https://github.com/valkey-io/valkey-glide/pull/1920)) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 1224763f06..9c6297f0ad 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -187,6 +187,8 @@ import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArrayBinary; +import static glide.utils.ArrayTransformUtils.convertNestedArrayToKeyValueGlideStringArray; +import static glide.utils.ArrayTransformUtils.convertNestedArrayToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.mapGeoDataToArray; import static glide.utils.ArrayTransformUtils.mapGeoDataToGlideStringArray; @@ -2588,12 +2590,23 @@ public CompletableFuture xadd(@NonNull String key, @NonNull Map xadd(@NonNull String key, @NonNull String[][] values) { + return xadd(key, values, StreamAddOptions.builder().build()); + } + @Override public CompletableFuture xadd( @NonNull GlideString key, @NonNull Map values) { return xadd(key, values, StreamAddOptionsBinary.builder().build()); } + @Override + public CompletableFuture xadd( + @NonNull GlideString key, @NonNull GlideString[][] values) { + return xadd(key, values, StreamAddOptionsBinary.builder().build()); + } + @Override public CompletableFuture xadd( @NonNull String key, @NonNull Map values, @NonNull StreamAddOptions options) { @@ -2603,6 +2616,16 @@ public CompletableFuture xadd( return commandManager.submitNewCommand(XAdd, arguments, this::handleStringOrNullResponse); } + @Override + public CompletableFuture xadd( + @NonNull String key, @NonNull String[][] values, @NonNull StreamAddOptions options) { + String[] arguments = + ArrayUtils.addAll( + ArrayUtils.addFirst(options.toArgs(), key), + convertNestedArrayToKeyValueStringArray(values)); + return commandManager.submitNewCommand(XAdd, arguments, this::handleStringOrNullResponse); + } + @Override public CompletableFuture xadd( @NonNull GlideString key, @@ -2618,6 +2641,21 @@ public CompletableFuture xadd( return commandManager.submitNewCommand(XAdd, arguments, this::handleGlideStringOrNullResponse); } + @Override + public CompletableFuture xadd( + @NonNull GlideString key, + @NonNull GlideString[][] values, + @NonNull StreamAddOptionsBinary options) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(convertNestedArrayToKeyValueGlideStringArray(values)) + .toArray(); + + return commandManager.submitNewCommand(XAdd, arguments, this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture>> xread( @NonNull Map keysAndIds) { diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index c88bda1201..a62000c3c8 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -29,7 +29,8 @@ public interface StreamBaseCommands { /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(String, String[][])}. * * @see valkey.io for details. * @param key The key of the stream. @@ -45,7 +46,25 @@ public interface StreamBaseCommands { /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @return The id of the added entry. + * @example + *
{@code
+     * String streamId = client.xadd("key", new String[][] {{"name", "Sara"}, {"surname", "OConnor"}}).get();
+     * System.out.println("Stream: " + streamId);
+     * }
+ */ + CompletableFuture xadd(String key, String[][] values); + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(GlideString, GlideString[][])}. * * @see valkey.io for details. * @param key The key of the stream. @@ -61,7 +80,25 @@ public interface StreamBaseCommands { /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @return The id of the added entry. + * @example + *
{@code
+     * String streamId = client.xadd(gs("key"), new String[][] {{gs("name"), gs("Sara")}, {gs("surname"), gs("OConnor")}}).get();
+     * System.out.println("Stream: " + streamId);
+     * }
+ */ + CompletableFuture xadd(GlideString key, GlideString[][] values); + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(String, String[][], StreamAddOptions)}. * * @see valkey.io for details. * @param key The key of the stream. @@ -73,10 +110,10 @@ public interface StreamBaseCommands { * @example *
{@code
      * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
-     * StreamAddOptions options = StreamAddOptions.builder().id("sid").makeStream(Boolean.FALSE).build();
+     * StreamAddOptions options = StreamAddOptions.builder().id("1-0").makeStream(Boolean.FALSE).build();
      * String streamId = client.xadd("key", Map.of("name", "Sara", "surname", "OConnor"), options).get();
      * if (streamId != null) {
-     *     assert streamId.equals("sid");
+     *     assert streamId.equals("1-0");
      * }
      * }
*/ @@ -84,7 +121,32 @@ public interface StreamBaseCommands { /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return The id of the added entry, or null if {@link + * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream + * with the matching key exists. + * @example + *
{@code
+     * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
+     * StreamAddOptions options = StreamAddOptions.builder().id("1-0").makeStream(Boolean.FALSE).build();
+     * String streamId = client.xadd("key", new String[][] {{"name", "Sara"}, {"surname", "OConnor"}}, options).get();
+     * if (streamId != null) {
+     *     assert streamId.equals("1-0");
+     * }
+     * }
+ */ + CompletableFuture xadd(String key, String[][] values, StreamAddOptions options); + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(GlideString, GlideString[][], StreamAddOptionsBinary)}. * * @see valkey.io for details. * @param key The key of the stream. @@ -96,16 +158,41 @@ public interface StreamBaseCommands { * @example *
{@code
      * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
-     * StreamAddOptionsBinary options = StreamAddOptions.builder().id(gs("sid")).makeStream(Boolean.FALSE).build();
+     * StreamAddOptionsBinary options = StreamAddOptions.builder().id(gs("1-0")).makeStream(Boolean.FALSE).build();
      * String streamId = client.xadd(gs("key"), Map.of(gs("name"), gs("Sara"), gs("surname"), gs("OConnor")), options).get();
      * if (streamId != null) {
-     *     assert streamId.equals("sid");
+     *     assert streamId.equals("1-0");
      * }
      * }
*/ CompletableFuture xadd( GlideString key, Map values, StreamAddOptionsBinary options); + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return The id of the added entry, or null if {@link + * StreamAddOptionsBinaryBuilder#makeStream(Boolean)} is set to false and no + * stream with the matching key exists. + * @example + *
{@code
+     * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
+     * StreamAddOptionsBinary options = StreamAddOptions.builder().id(gs("1-0")).makeStream(Boolean.FALSE).build();
+     * String streamId = client.xadd(gs("key"), new GlideString[][] {{gs("name"), gs("Sara")}, {gs("surname"), gs("OConnor")}}, options).get();
+     * if (streamId != null) {
+     *     assert streamId.equals("1-0");
+     * }
+     * }
+ */ + CompletableFuture xadd( + GlideString key, GlideString[][] values, StreamAddOptionsBinary options); + /** * Reads entries from the given streams. * diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index ec2ab02b0c..6766617426 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -215,6 +215,7 @@ import static glide.utils.ArrayTransformUtils.flattenAllKeysFollowedByAllValues; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArray; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArrayValueFirst; +import static glide.utils.ArrayTransformUtils.flattenNestedArrayToGlideStringArray; import static glide.utils.ArrayTransformUtils.mapGeoDataToGlideStringArray; import command_request.CommandRequestOuterClass.Command; @@ -3343,7 +3344,8 @@ public T zinterWithScores( /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(ArgType, ArgType[][])}. * * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. @@ -3358,7 +3360,24 @@ public T xadd(@NonNull ArgType key, @NonNull Map val /** * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @return Command Response - The id of the added entry. + */ + public T xadd(@NonNull ArgType key, @NonNull ArgType[][] values) { + return xadd(key, values, StreamAddOptions.builder().build()); + } + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. To add entries with duplicate + * keys, use {@link #xadd(ArgType, ArgType[][], StreamAddOptions)}. * * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. @@ -3385,6 +3404,34 @@ public T xadd( return getThis(); } + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. This method overload allows + * entries with duplicate keys to be added. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return Command Response - The id of the added entry, or null if {@link + * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream + * with the matching key exists. + */ + public T xadd( + @NonNull ArgType key, @NonNull ArgType[][] values, @NonNull StreamAddOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XAdd, + newArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(flattenNestedArrayToGlideStringArray(values)))); + return getThis(); + } + /** * Reads entries from the given streams. * diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java index fccdac14f3..c89545faef 100644 --- a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -44,6 +44,44 @@ public static GlideString[] convertMapToKeyValueGlideStringArray(Map Stream.of(entry[0], entry[1])) + .toArray(String[]::new); + } + + /** + * Converts a nested array of GlideString keys and values of any type in to an array of + * GlideStrings with alternating keys and values. + * + * @param args Nested array of GlideString keys to values of any type to convert. + * @return Array of strings [key1, gs(value1.toString()), key2, gs(value2.toString()), ...]. + */ + public static GlideString[] convertNestedArrayToKeyValueGlideStringArray(GlideString[][] args) { + for (GlideString[] entry : args) { + if (entry.length != 2) { + throw new IllegalArgumentException( + "Array entry had the wrong length. Expected length 2 but got length " + entry.length); + } + } + return Arrays.stream(args) + .flatMap(entry -> Stream.of(entry[0], GlideString.gs(entry[1].toString()))) + .toArray(GlideString[]::new); + } + /** * Converts a map of string keys and values of any type into an array of strings with alternating * values and keys. @@ -250,6 +288,25 @@ public static GlideString[] flattenMapToGlideStringArray(Map args) { .toArray(GlideString[]::new); } + /** + * Converts a nested array of any type of keys and values in to an array of GlideString with + * alternating keys and values. + * + * @param args Nested array of keys to values of any type to convert. + * @return Array of GlideString [key1, value1, key2, value2, ...]. + */ + public static GlideString[] flattenNestedArrayToGlideStringArray(T[][] args) { + for (T[] entry : args) { + if (entry.length != 2) { + throw new IllegalArgumentException( + "Array entry had the wrong length. Expected length 2 but got length " + entry.length); + } + } + return Arrays.stream(args) + .flatMap(entry -> Stream.of(GlideString.of(entry[0]), GlideString.of(entry[1]))) + .toArray(GlideString[]::new); + } + /** * Converts a map of any type of keys and values in to an array of GlideString with alternating * values and keys. diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 2727db7346..cb29738bbb 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -271,6 +271,8 @@ import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArrayBinary; +import static glide.utils.ArrayTransformUtils.convertNestedArrayToKeyValueGlideStringArray; +import static glide.utils.ArrayTransformUtils.convertNestedArrayToKeyValueStringArray; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -7174,6 +7176,59 @@ public void xadd_returns_success() { assertEquals(returnId, response.get()); } + @SneakyThrows + @Test + public void xadd_nested_array_returns_success() { + // setup + String key = "testKey"; + String[][] fieldValues = {{"testField1", "testValue1"}, {"testField2", "testValue2"}}; + String[] fieldValuesArgs = convertNestedArrayToKeyValueStringArray(fieldValues); + String[] arguments = new String[] {key, "*"}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + String returnId = "testId"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + + @SneakyThrows + @Test + public void xadd_nested_array_with_options_returns_success() { + // setup + String key = "testKey"; + String[][] fieldValues = {{"testField1", "testValue1"}, {"testField2", "testValue2"}}; + String[] fieldValuesArgs = convertNestedArrayToKeyValueStringArray(fieldValues); + String[] arguments = new String[] {key, "*"}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + String returnId = "testId"; + StreamAddOptions options = StreamAddOptions.builder().build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues, options); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + @SneakyThrows @Test public void xadd_binary_returns_success() { @@ -7202,6 +7257,63 @@ public void xadd_binary_returns_success() { assertEquals(returnId, response.get()); } + @SneakyThrows + @Test + public void xadd_nested_array_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[][] fieldValues = { + {gs("testField1"), gs("testValue1")}, {gs("testField2"), gs("testValue2")} + }; + GlideString[] fieldValuesArgs = convertNestedArrayToKeyValueGlideStringArray(fieldValues); + GlideString[] arguments = new GlideString[] {key, gs("*")}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + GlideString returnId = gs("testId"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + + @SneakyThrows + @Test + public void xadd_nested_array_with_options_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[][] fieldValues = { + {gs("testField1"), gs("testValue1")}, {gs("testField2"), gs("testValue2")} + }; + GlideString[] fieldValuesArgs = convertNestedArrayToKeyValueGlideStringArray(fieldValues); + GlideString[] arguments = new GlideString[] {key, gs("*")}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + GlideString returnId = gs("testId"); + StreamAddOptionsBinary options = StreamAddOptionsBinary.builder().build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues, options); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + private static List getStreamAddOptions() { return List.of( Arguments.of( diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 00aed8661f..aa49a6849b 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -769,6 +769,15 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.xadd("key", Map.of("field1", "foo1"), StreamAddOptions.builder().id("id").build()); results.add(Pair.of(XAdd, buildArgs("key", "id", "field1", "foo1"))); + transaction.xadd("key", new String[][] {new String[] {"field1", "foo1"}}); + results.add(Pair.of(XAdd, buildArgs("key", "*", "field1", "foo1"))); + + transaction.xadd( + "key", + new String[][] {new String[] {"field1", "foo1"}}, + StreamAddOptions.builder().id("id").build()); + results.add(Pair.of(XAdd, buildArgs("key", "id", "field1", "foo1"))); + transaction.xtrim("key", new MinId(true, "id")); results.add( Pair.of(XTrim, buildArgs("key", TRIM_MINID_VALKEY_API, TRIM_EXACT_VALKEY_API, "id"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 439fa39ff1..9832ec2e7c 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5769,6 +5769,112 @@ public void bzmpop_binary_timeout_check(BaseClient client) { } } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_duplicate_entry_keys(BaseClient client) { + String key = UUID.randomUUID().toString(); + String field = UUID.randomUUID().toString(); + String foo1 = "foo1"; + String bar1 = "bar1"; + + String[][] entry = new String[][] {{field, foo1}, {field, bar1}}; + String streamId = client.xadd(key, entry).get(); + // get everything from the stream + Map result = client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(1, result.size()); + String[][] actualEntry = result.get(streamId); + assertDeepEquals(entry, actualEntry); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_duplicate_entry_keys_with_options(BaseClient client) { + String key = UUID.randomUUID().toString(); + String field = UUID.randomUUID().toString(); + String foo1 = "foo1"; + String bar1 = "bar1"; + + String[][] entry = new String[][] {{field, foo1}, {field, bar1}}; + String streamId = client.xadd(key, entry, StreamAddOptions.builder().build()).get(); + // get everything from the stream + Map result = client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(1, result.size()); + String[][] actualEntry = result.get(streamId); + assertDeepEquals(entry, actualEntry); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_duplicate_entry_keys_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field = gs(UUID.randomUUID().toString()); + GlideString foo1 = gs("foo1"); + GlideString bar1 = gs("bar1"); + + GlideString[][] entry = new GlideString[][] {{field, foo1}, {field, bar1}}; + GlideString streamId = client.xadd(key, entry).get(); + // get everything from the stream + Map result = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(1, result.size()); + GlideString[][] actualEntry = result.get(streamId); + assertDeepEquals(entry, actualEntry); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_duplicate_entry_keys_with_options_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field = gs(UUID.randomUUID().toString()); + GlideString foo1 = gs("foo1"); + GlideString bar1 = gs("bar1"); + + GlideString[][] entry = new GlideString[][] {{field, foo1}, {field, bar1}}; + GlideString streamId = client.xadd(key, entry, StreamAddOptionsBinary.builder().build()).get(); + // get everything from the stream + Map result = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(1, result.size()); + GlideString[][] actualEntry = result.get(streamId); + assertDeepEquals(entry, actualEntry); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_wrong_length_entries(BaseClient client) { + String key = UUID.randomUUID().toString(); + String timestamp = "0-1"; + + // Entry too long + assertThrows( + IllegalArgumentException.class, + () -> + client + .xadd( + key, + new String[][] { + new String[] {"field1", "foo1"}, new String[] {"field2", "bar2", "oh no"} + }, + StreamAddOptions.builder().id(timestamp).build()) + .get()); + + // Entry too short + assertThrows( + IllegalArgumentException.class, + () -> + client + .xadd( + key, + new String[][] {new String[] {"field1", "foo1"}, new String[] {"oh no"}}, + StreamAddOptions.builder().id(timestamp).build()) + .get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 110e71e29b..96b0c9f894 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -815,6 +815,7 @@ private static Object[] streamCommands(BaseTransaction transaction) { final String streamKey1 = "{streamKey}-1-" + UUID.randomUUID(); final String streamKey2 = "{streamKey}-2-" + UUID.randomUUID(); final String streamKey3 = "{streamKey}-3-" + UUID.randomUUID(); + final String streamKey4 = "{streamKey}-4-" + UUID.randomUUID(); final String groupName1 = "{groupName}-1-" + UUID.randomUUID(); final String groupName2 = "{groupName}-2-" + UUID.randomUUID(); final String groupName3 = "{groupName}-2-" + UUID.randomUUID(); @@ -824,6 +825,10 @@ private static Object[] streamCommands(BaseTransaction transaction) { .xadd(streamKey1, Map.of("field1", "value1"), StreamAddOptions.builder().id("0-1").build()) .xadd(streamKey1, Map.of("field2", "value2"), StreamAddOptions.builder().id("0-2").build()) .xadd(streamKey1, Map.of("field3", "value3"), StreamAddOptions.builder().id("0-3").build()) + .xadd( + streamKey4, + new String[][] {{"field4", "value4"}, {"field4", "value5"}}, + StreamAddOptions.builder().id("0-4").build()) .xlen(streamKey1) .xread(Map.of(streamKey1, "0-2")) .xread(Map.of(streamKey1, "0-2"), StreamReadOptions.builder().count(1L).build()) @@ -896,6 +901,8 @@ private static Object[] streamCommands(BaseTransaction transaction) { "0-1", // xadd(streamKey1, Map.of("field1", "value1"), ... .id("0-1").build()); "0-2", // xadd(streamKey1, Map.of("field2", "value2"), ... .id("0-2").build()); "0-3", // xadd(streamKey1, Map.of("field3", "value3"), ... .id("0-3").build()); + "0-4", // xadd(streamKey4, new String[][] {{"field4", "value4"}, {"field4", "value5"}}), + // ... .id("0-4").build()); 3L, // xlen(streamKey1) Map.of( streamKey1, From 46254c1b839891d87399d9e7bb650686e48baadd Mon Sep 17 00:00:00 2001 From: ort-bot Date: Wed, 24 Jul 2024 00:22:08 +0000 Subject: [PATCH 040/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 2093 +------------------------ java/THIRD_PARTY_LICENSES_JAVA | 2107 +------------------------- node/THIRD_PARTY_LICENSES_NODE | 2103 +------------------------ python/THIRD_PARTY_LICENSES_PYTHON | 2095 +------------------------ 4 files changed, 77 insertions(+), 8321 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 0bc2e35712..ddcf381da8 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -13530,7 +13530,7 @@ the following restrictions: ---- -Package: mio:0.8.11 +Package: mio:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -20444,7 +20444,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.5 +Package: rustls-webpki:0.102.6 The following copyrights and licenses were found in the source code of this package: @@ -22095,7 +22095,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: sha1_smol:1.0.0 +Package: sha1_smol:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -22969,7 +22969,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.71 +Package: syn:2.0.72 The following copyrights and licenses were found in the source code of this package: @@ -23198,7 +23198,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror:1.0.62 +Package: thiserror:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -23427,7 +23427,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.62 +Package: thiserror-impl:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -25070,7 +25070,7 @@ the following restrictions: ---- -Package: tokio:1.38.1 +Package: tokio:1.39.1 The following copyrights and licenses were found in the source code of this package: @@ -25095,7 +25095,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.3.0 +Package: tokio-macros:2.4.0 The following copyrights and licenses were found in the source code of this package: @@ -29079,1839 +29079,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-sys:0.48.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-sys:0.52.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_i686_gnu:0.48.5 +Package: windows-sys:0.52.0 The following copyrights and licenses were found in the source code of this package: @@ -31140,7 +29308,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.6 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31369,7 +29537,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.6 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31598,7 +29766,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.48.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31827,7 +29995,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.6 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32056,7 +30224,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.48.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32285,7 +30453,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.6 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32514,7 +30682,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.48.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32972,235 +31140,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - Package: windows_x86_64_msvc:0.52.6 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 a363964613..1c5176b1ed 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -14425,7 +14425,7 @@ the following restrictions: ---- -Package: mio:0.8.11 +Package: mio:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -21339,7 +21339,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.5 +Package: rustls-webpki:0.102.6 The following copyrights and licenses were found in the source code of this package: @@ -22990,7 +22990,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: sha1_smol:1.0.0 +Package: sha1_smol:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -23864,7 +23864,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.71 +Package: syn:2.0.72 The following copyrights and licenses were found in the source code of this package: @@ -24093,7 +24093,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror:1.0.62 +Package: thiserror:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -24322,7 +24322,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.62 +Package: thiserror-impl:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -25965,7 +25965,7 @@ the following restrictions: ---- -Package: tokio:1.38.1 +Package: tokio:1.39.1 The following copyrights and licenses were found in the source code of this package: @@ -25990,7 +25990,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.3.0 +Package: tokio-macros:2.4.0 The following copyrights and licenses were found in the source code of this package: @@ -30203,1839 +30203,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-sys:0.48.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-sys:0.52.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.42.2 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.42.2 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.42.2 +Package: windows-sys:0.52.0 The following copyrights and licenses were found in the source code of this package: @@ -32264,7 +30432,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.48.5 +Package: windows-targets:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -32493,7 +30661,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.6 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32722,7 +30890,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.42.2 +Package: windows_aarch64_gnullvm:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -32951,7 +31119,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.48.5 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33180,7 +31348,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.6 +Package: windows_aarch64_msvc:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -33409,7 +31577,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.6 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33638,7 +31806,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.42.2 +Package: windows_i686_gnu:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -33867,7 +32035,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.48.5 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34096,7 +32264,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.6 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34325,7 +32493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.42.2 +Package: windows_i686_msvc:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -34554,7 +32722,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.48.5 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34783,7 +32951,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.6 +Package: windows_x86_64_gnu:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -35012,7 +33180,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.42.2 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35241,7 +33409,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.48.5 +Package: windows_x86_64_gnullvm:0.42.2 The following copyrights and licenses were found in the source code of this package: @@ -35928,235 +34096,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - Package: windows_x86_64_msvc:0.52.6 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 8f750e1968..3d9f710c22 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -13104,7 +13104,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libloading:0.8.4 +Package: libloading:0.8.5 The following copyrights and licenses were found in the source code of this package: @@ -14114,7 +14114,7 @@ the following restrictions: ---- -Package: mio:0.8.11 +Package: mio:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -14214,7 +14214,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive:2.16.9 +Package: napi-derive:2.16.10 The following copyrights and licenses were found in the source code of this package: @@ -14239,7 +14239,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive-backend:1.0.71 +Package: napi-derive-backend:1.0.72 The following copyrights and licenses were found in the source code of this package: @@ -21840,7 +21840,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.5 +Package: rustls-webpki:0.102.6 The following copyrights and licenses were found in the source code of this package: @@ -23720,7 +23720,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: sha1_smol:1.0.0 +Package: sha1_smol:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -24594,7 +24594,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.71 +Package: syn:2.0.72 The following copyrights and licenses were found in the source code of this package: @@ -24823,7 +24823,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror:1.0.62 +Package: thiserror:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -25052,7 +25052,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.62 +Package: thiserror-impl:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -27153,7 +27153,7 @@ the following restrictions: ---- -Package: tokio:1.38.1 +Package: tokio:1.39.1 The following copyrights and licenses were found in the source code of this package: @@ -27178,7 +27178,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.3.0 +Package: tokio-macros:2.4.0 The following copyrights and licenses were found in the source code of this package: @@ -31391,1839 +31391,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-sys:0.48.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-sys:0.52.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_i686_gnu:0.48.5 +Package: windows-sys:0.52.0 The following copyrights and licenses were found in the source code of this package: @@ -33452,7 +31620,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.6 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33681,7 +31849,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.6 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33910,7 +32078,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.48.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34139,7 +32307,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.6 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34368,7 +32536,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.48.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34597,7 +32765,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.6 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34826,7 +32994,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.48.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35284,235 +33452,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - Package: windows_x86_64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -38154,7 +36093,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: is-core-module:2.14.0 +Package: is-core-module:2.15.0 The following copyrights and licenses were found in the source code of this package: @@ -39964,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:20.14.11 +Package: @types:node:20.14.12 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 fe669c485d..3cb41c8027 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -14221,7 +14221,7 @@ the following restrictions: ---- -Package: mio:0.8.11 +Package: mio:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -17756,7 +17756,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: portable-atomic:1.6.0 +Package: portable-atomic:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -22509,7 +22509,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.5 +Package: rustls-webpki:0.102.6 The following copyrights and licenses were found in the source code of this package: @@ -24160,7 +24160,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: sha1_smol:1.0.0 +Package: sha1_smol:1.0.1 The following copyrights and licenses were found in the source code of this package: @@ -25034,7 +25034,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.71 +Package: syn:2.0.72 The following copyrights and licenses were found in the source code of this package: @@ -25487,7 +25487,7 @@ Software. ---- -Package: thiserror:1.0.62 +Package: thiserror:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -25716,7 +25716,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.62 +Package: thiserror-impl:1.0.63 The following copyrights and licenses were found in the source code of this package: @@ -27359,7 +27359,7 @@ the following restrictions: ---- -Package: tokio:1.38.1 +Package: tokio:1.39.1 The following copyrights and licenses were found in the source code of this package: @@ -27384,7 +27384,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.3.0 +Package: tokio-macros:2.4.0 The following copyrights and licenses were found in the source code of this package: @@ -31597,1839 +31597,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-sys:0.48.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-sys:0.52.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows-targets:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_gnullvm:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_aarch64_msvc:0.52.6 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: windows_i686_gnu:0.48.5 +Package: windows-sys:0.52.0 The following copyrights and licenses were found in the source code of this package: @@ -33658,7 +31826,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.6 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33887,7 +32055,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.6 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34116,7 +32284,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.48.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34345,7 +32513,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.6 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34574,7 +32742,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.48.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34803,7 +32971,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.6 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35032,7 +33200,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.48.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35490,235 +33658,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.48.5 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - Package: windows_x86_64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: From cd2b419a4af63a04cc0b01d0a13a01a439f53ab3 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 24 Jul 2024 07:50:30 -0700 Subject: [PATCH 041/236] Node: Add `tsconfig` for tests. (#2001) Add `tsconfig`. Signed-off-by: Yury-Fridlyand --- node/tests/tsconfig.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 node/tests/tsconfig.json diff --git a/node/tests/tsconfig.json b/node/tests/tsconfig.json new file mode 100644 index 0000000000..f7b57f9a00 --- /dev/null +++ b/node/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../../" + }, + "include": ["*.ts", "./*.test.ts"] +} From 0b7309cdedcca910cc0f17875d6e7b5ba40c1b1d Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 24 Jul 2024 07:51:12 -0700 Subject: [PATCH 042/236] Python client CI: Split lint and testing in CI + add reporting. (#1976) * Split lint and testing in CI + add reporting. (#238) Signed-off-by: Yury-Fridlyand --- .github/workflows/python.yml | 268 ++++++++++++++++++----------------- python/.gitignore | 1 + python/requirements.txt | 1 + 3 files changed, 137 insertions(+), 133 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 95dadbc0a0..7e453178a6 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -40,20 +40,20 @@ permissions: jobs: load-engine-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.load-engine-matrix.outputs.matrix }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Load the engine matrix - id: load-engine-matrix - shell: bash - run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT - - test-ubuntu-latest: runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + + test: + runs-on: ${{ matrix.host.RUNNER }} needs: load-engine-matrix timeout-minutes: 35 strategy: @@ -65,7 +65,18 @@ jobs: # - "3.9" # - "3.10" # - "3.11" - - "3.12" + - "3.12" + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + # - { + # OS: macos, + # RUNNER: macos-latest, + # TARGET: aarch64-apple-darwin + # } steps: - uses: actions/checkout@v4 @@ -81,31 +92,13 @@ jobs: working-directory: ./python run: | python -m pip install --upgrade pip - pip install flake8 isort black mypy-protobuf - - - name: Lint with isort - working-directory: ./python - run: | - isort . --profile black --check --diff - - - name: Lint with flake8 - working-directory: ./python - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --extend-ignore=E230 --exclude=python/glide/protobuf,.env/* - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=12 --max-line-length=127 --statistics --extend-ignore=E230 --exclude=python/glide/protobuf,.env/* - - - name: Lint with black - working-directory: ./python - run: | - black --check --diff . + pip install mypy-protobuf - name: Build Python wrapper uses: ./.github/workflows/build-python-wrapper with: - os: "ubuntu" - target: "x86_64-unknown-linux-gnu" + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} engine-version: ${{ matrix.engine.version }} @@ -125,53 +118,84 @@ jobs: run: | source .env/bin/activate cd python/tests/ - pytest --asyncio-mode=auto + pytest --asyncio-mode=auto --html=pytest_report.html --self-contained-html - uses: ./.github/workflows/test-benchmark with: language-flag: -python - test-pubsub-ubuntu-latest: - runs-on: ubuntu-latest - needs: load-engine-matrix - timeout-minutes: 35 - strategy: - fail-fast: false - matrix: - engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} - python: - # - "3.8" - # - "3.9" - # - "3.10" - # - "3.11" - - "3.12" - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python }} - - - name: Build Python wrapper - uses: ./.github/workflows/build-python-wrapper - with: - os: "ubuntu" - target: "x86_64-unknown-linux-gnu" - github-token: ${{ secrets.GITHUB_TOKEN }} - engine-version: ${{ matrix.engine.version }} + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-report-python-${{ matrix.python }}-${{ matrix.engine.type }}-${{ matrix.engine.version }}-${{ matrix.host.RUNNER }} + path: | + python/python/tests/pytest_report.html + utils/clusters/** + benchmarks/results/** + + test-pubsub: + runs-on: ${{ matrix.host.RUNNER }} + needs: load-engine-matrix + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + python: + # - "3.8" + # - "3.9" + # - "3.10" + # - "3.11" + - "3.12" + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + # - { + # OS: macos, + # RUNNER: macos-latest, + # TARGET: aarch64-apple-darwin + # } - - name: Test pubsub with pytest - working-directory: ./python - run: | - source .env/bin/activate - cd python/tests/ - pytest --asyncio-mode=auto -k test_pubsub + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Build Python wrapper + uses: ./.github/workflows/build-python-wrapper + with: + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} + github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: ${{ matrix.engine.version }} + + - name: Test pubsub with pytest + working-directory: ./python + run: | + source .env/bin/activate + cd python/tests/ + pytest --asyncio-mode=auto -k test_pubsub --html=pytest_report.html --self-contained-html + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: pubsub-test-report-python-${{ matrix.python }}-${{ matrix.engine.type }}-${{ matrix.engine.version }}-${{ matrix.host.RUNNER }} + path: | + python/python/tests/pytest_report.html - lint-rust: + lint: runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -179,69 +203,38 @@ jobs: with: submodules: recursive - - uses: ./.github/workflows/lint-rust + - name: lint rust + uses: ./.github/workflows/lint-rust with: cargo-toml-folder: ./python - name: lint python-rust - - # test-macos-latest: - # runs-on: macos-latest - # needs: load-engine-matrix - # timeout-minutes: 35 - # strategy: - # fail-fast: false - # matrix: - # engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} - # steps: - # - uses: actions/checkout@v4 - # with: - # submodules: recursive - # - name: Set up Homebrew - # uses: Homebrew/actions/setup-homebrew@master - - # - name: Build Python wrapper - # uses: ./.github/workflows/build-python-wrapper - # with: - # os: "macos" - # target: "aarch64-apple-darwin" - # github-token: ${{ secrets.GITHUB_TOKEN }} - # engine-version: ${{ matrix.engine.version }} - # - name: Test with pytest - # working-directory: ./python - # run: | - # source .env/bin/activate - # pytest --asyncio-mode=auto + - name: Install dependencies + if: always() + working-directory: ./python + run: | + python -m pip install --upgrade pip + pip install flake8 isort black - # test-pubsub-macos-latest: - # runs-on: macos-latest - # needs: load-engine-matrix - # timeout-minutes: 35 - # strategy: - # fail-fast: false - # matrix: - # engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} - # steps: - # - uses: actions/checkout@v4 - # with: - # submodules: recursive - # - name: Set up Homebrew - # uses: Homebrew/actions/setup-homebrew@master + - name: Lint python with isort + if: always() + working-directory: ./python + run: | + isort . --profile black --check --diff - # - name: Build Python wrapper - # uses: ./.github/workflows/build-python-wrapper - # with: - # os: "macos" - # target: "aarch64-apple-darwin" - # github-token: ${{ secrets.GITHUB_TOKEN }} - # engine-version: ${{ matrix.engine.version }} + - name: Lint python with flake8 + if: always() + working-directory: ./python + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --extend-ignore=E230 --exclude=python/glide/protobuf,.env/* + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=12 --max-line-length=127 --statistics --extend-ignore=E230 --exclude=python/glide/protobuf,.env/* - # - name: Test pubsub with pytest - # working-directory: ./python - # run: | - # source .env/bin/activate - # cd python/tests/ - # pytest --asyncio-mode=auto -k test_pubsub + - name: Lint python with black + if: always() + working-directory: ./python + run: | + black --check --diff . build-amazonlinux-latest: runs-on: ubuntu-latest @@ -279,4 +272,13 @@ jobs: working-directory: ./python run: | source .env/bin/activate - pytest --asyncio-mode=auto -m smoke_test + pytest --asyncio-mode=auto -m smoke_test --html=pytest_report.html --self-contained-html + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: smoke-test-report-amazon-linux + path: | + python/python/tests/pytest_report.html diff --git a/python/.gitignore b/python/.gitignore index 0b5ca61d21..17dcb39f70 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -50,6 +50,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +*.html # Translations *.mo diff --git a/python/requirements.txt b/python/requirements.txt index b114a23fe4..12eb7d5e2b 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -5,3 +5,4 @@ protobuf==3.20.* pytest==7.1.2 pytest-asyncio==0.19.0 typing_extensions==4.8.0 +pytest-html From d2d5585b56192cadce2910b82a51f569421a6ea6 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 24 Jul 2024 09:38:50 -0700 Subject: [PATCH 043/236] Node: added FCALL and FCALL_RO commands Signed-off-by: Yi-Pin Chen --- node/src/BaseClient.ts | 63 ++++++++++++++++++++++- node/src/Commands.ts | 72 +++++++++++++++++++++++---- node/src/GlideClusterClient.ts | 63 +++++++++++++++++++++++ node/src/Transaction.ts | 38 ++++++++++++++ node/tests/RedisClient.test.ts | 30 +++-------- node/tests/RedisClusterClient.test.ts | 21 ++++---- node/tests/TestUtilities.ts | 4 ++ 7 files changed, 245 insertions(+), 46 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9119493f3c..d5d43709e3 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -13,6 +13,7 @@ import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, ExpireOptions, + GeoUnit, InsertPosition, KeyWeight, RangeByIndex, @@ -34,10 +35,12 @@ import { createExists, createExpire, createExpireAt, + createFCall, + createFCallReadOnly, createGeoAdd, createGeoDist, - createGeoPos, createGeoHash, + createGeoPos, createGet, createGetBit, createGetDel, @@ -129,7 +132,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - GeoUnit, } from "./Commands"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; @@ -3330,6 +3332,63 @@ export class BaseClient { return this.createWritePromise(createObjectRefcount(key)); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcall("Deep_Thought"); + * console.log(response); // Output: Returns the function's return value. + * ``` + */ + public fcall( + func: string, + keys?: string[], + args?: string[], + ): Promise { + return this.createWritePromise(createFCall(func, keys, args)); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallReadOnly("Deep_Thought", ["key1"], ["Answer", "to", "the", + * "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"]); + * console.log(response); // Output: 42 # The return value on the function that was execute. + * ``` + */ + public fcallReadonly( + func: string, + keys?: string[], + args?: string[], + ): Promise { + return this.createWritePromise(createFCallReadOnly(func, keys, args)); + } + /** * 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 diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b34e7be2b1..fe0b2fa50c 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1584,6 +1584,56 @@ export function createBLPop( return createCommand(RequestType.BLPop, args); } +/** + * @internal + */ +export function createFCall( + func: string, + keys?: string[], + args?: string[], +): command_request.Command { + const params: string[] = []; + params.push(func); + + if (keys !== undefined) { + params.push(keys.length.toString()); + params.push(...keys); + } else { + params.push("0"); + } + + if (args !== undefined) { + params.push(...args); + } + + return createCommand(RequestType.FCall, params); +} + +/** + * @internal + */ +export function createFCallReadOnly( + func: string, + keys?: string[], + args?: string[], +): command_request.Command { + const params: string[] = []; + params.push(func); + + if (keys !== undefined) { + params.push(keys.length.toString()); + params.push(...keys); + } else { + params.push("0"); + } + + if (args !== undefined) { + params.push(...args); + } + + return createCommand(RequestType.FCallReadOnly, params); +} + /** * @internal */ @@ -1593,6 +1643,17 @@ export function createFunctionDelete( return createCommand(RequestType.FunctionDelete, [libraryCode]); } +/** + * @internal + */ +export function createFunctionFlush(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FunctionFlush, [mode.toString()]); + } else { + return createCommand(RequestType.FunctionFlush, []); + } +} + /** * @internal */ @@ -1616,17 +1677,6 @@ export function createBitCount( return createCommand(RequestType.BitCount, args); } -/** - * @internal - */ -export function createFunctionFlush(mode?: FlushMode): command_request.Command { - if (mode) { - return createCommand(RequestType.FunctionFlush, [mode.toString()]); - } else { - return createCommand(RequestType.FunctionFlush, []); - } -} - export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d552469812..7a94cd3514 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -21,6 +21,8 @@ import { createCustomCommand, createDBSize, createEcho, + createFCall, + createFCallReadOnly, createFlushAll, createFlushDB, createFunctionDelete, @@ -659,6 +661,67 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @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 The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallRoute("Deep_Thought", undefined, "randomNode"); + * console.log(response); // Output: Returns the function's return value. + * ``` + */ + public fcallRoute( + func: string, + args?: string[], + route?: Routes, + ): Promise { + return this.createWritePromise( + createFCall(func, undefined, args), + toProtobufRoute(route), + ); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @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 The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallReadOnly("Deep_Thought", ["Answer", "to", "the", "Ultimate", + * "Question", "of", "Life,", "the", "Universe,", "and", "Everything"], "randomNode"); + * console.log(response); // Output: 42 # The return value on the function that was execute. + * ``` + */ + public fcallReadonlyRoute( + func: string, + args?: string[], + route?: Routes, + ): Promise { + return this.createWritePromise( + createFCallReadOnly(func, undefined, args), + toProtobufRoute(route), + ); + } + /** * Deletes a library and all its functions. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8a9b1b61e0..0d242d91ab 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -37,6 +37,8 @@ import { createExists, createExpire, createExpireAt, + createFCall, + createFCallReadOnly, createFlushAll, createFlushDB, createFunctionDelete, @@ -1854,6 +1856,42 @@ export class BaseTransaction> { return this.addAndReturn(createLolwut(options)); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param keys - A list of keys accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * + * Command Response - The invoked function's return value. + */ + public fcall(func: string, keys?: string[], args?: string[]): T { + return this.addAndReturn(createFCall(func, keys, args)); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param keys - A list of keys accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * + * Command Response - The invoked function's return value. + */ + public fcallReadonly(func: string, keys?: string[], args?: string[]): T { + return this.addAndReturn(createFCallReadOnly(func, keys, args)); + } + /** * Deletes a library and all its functions. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 853a8aa769..fe0b78e73f 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -397,19 +397,10 @@ describe("GlideClient", () => { checkSimple(await client.functionLoad(code)).toEqual(libName); checkSimple( - await client.customCommand([ - "FCALL", - funcName, - "0", - "one", - "two", - ]), + await client.fcall(funcName, undefined, ["one", "two"]), ).toEqual("one"); checkSimple( - await client.customCommand([ - "FCALL_RO", - funcName, - "0", + await client.fcallReadonly(funcName, undefined, [ "one", "two", ]), @@ -441,20 +432,11 @@ describe("GlideClient", () => { libName, ); - expect( - await client.customCommand([ - "FCALL", - func2Name, - "0", - "one", - "two", - ]), + checkSimple( + await client.fcall(func2Name, undefined, ["one", "two"]), ).toEqual(2); - expect( - await client.customCommand([ - "FCALL_RO", - func2Name, - "0", + checkSimple( + await client.fcallReadonly(func2Name, undefined, [ "one", "two", ]), diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index e039c96ad5..fb170a8fbb 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -610,8 +610,9 @@ describe("GlideClusterClient", () => { await client.functionLoad(code), ).toEqual(libName); // call functions from that library to confirm that it works - let fcall = await client.customCommand( - ["FCALL", funcName, "0", "one", "two"], + let fcall = await client.fcallRoute( + funcName, + ["one", "two"], route, ); checkClusterResponse( @@ -620,9 +621,9 @@ describe("GlideClusterClient", () => { (value) => checkSimple(value).toEqual("one"), ); - - fcall = await client.customCommand( - ["FCALL_RO", funcName, "0", "one", "two"], + fcall = await client.fcallReadonlyRoute( + funcName, + ["one", "two"], route, ); checkClusterResponse( @@ -659,8 +660,9 @@ describe("GlideClusterClient", () => { await client.functionLoad(newCode, true), ).toEqual(libName); - fcall = await client.customCommand( - ["FCALL", func2Name, "0", "one", "two"], + fcall = await client.fcallRoute( + func2Name, + ["one", "two"], route, ); checkClusterResponse( @@ -669,8 +671,9 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual(2), ); - fcall = await client.customCommand( - ["FCALL_RO", func2Name, "0", "one", "two"], + fcall = await client.fcallReadonlyRoute( + func2Name, + ["one", "two"], route, ); checkClusterResponse( diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4a009cef85..e350bef201 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -693,6 +693,10 @@ export async function transactionTest( args.push(libName); baseTransaction.functionLoad(code, true); args.push(libName); + baseTransaction.fcall(funcName, undefined, ["one", "two"]); + args.push("one"); + baseTransaction.fcallReadonly(funcName, undefined, ["one", "two"]); + args.push("one"); baseTransaction.functionDelete(libName); args.push("OK"); baseTransaction.functionFlush(); From 9cf271c04c30ac5e0478006529e9fc77241f1ca6 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 24 Jul 2024 09:55:40 -0700 Subject: [PATCH 044/236] Updated CHANGELOG Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/GlideClusterClient.ts | 2 +- node/src/Transaction.ts | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4afea615..4175ad6af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) * Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990)) * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) +* Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) #### Fixes diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 7a94cd3514..c7819b5064 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -706,7 +706,7 @@ export class GlideClusterClient extends BaseClient { * * @example * ```typescript - * const response = await client.fcallReadOnly("Deep_Thought", ["Answer", "to", "the", "Ultimate", + * const response = await client.fcallReadonlyRoute("Deep_Thought", ["Answer", "to", "the", "Ultimate", * "Question", "of", "Life,", "the", "Universe,", "and", "Everything"], "randomNode"); * console.log(response); // Output: 42 # The return value on the function that was execute. * ``` diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0d242d91ab..db48e03b9d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -5,6 +5,7 @@ import { AggregationType, ExpireOptions, + GeoUnit, InfoOptions, InsertPosition, KeyWeight, @@ -144,7 +145,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - GeoUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; @@ -1864,7 +1864,7 @@ export class BaseTransaction> { * since Valkey version 7.0.0. * * @param func - The function name. - * @param keys - A list of keys accessed by the function. To ensure the correct execution of functions, + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * @@ -1882,7 +1882,7 @@ export class BaseTransaction> { * since Valkey version 7.0.0. * * @param func - The function name. - * @param keys - A list of keys accessed by the function. To ensure the correct execution of functions, + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * From 9a69ac1356ee8bd31c78aeee60e2399d724ffbf0 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 24 Jul 2024 10:04:27 -0700 Subject: [PATCH 045/236] Node: rework server version check and update tests (#1993) * Fix IT. Signed-off-by: Yury-Fridlyand --- node/package.json | 3 +- node/tests/RedisClient.test.ts | 15 ++-- node/tests/RedisClusterClient.test.ts | 39 ++++++---- node/tests/SharedTests.ts | 108 ++++++++++---------------- node/tests/TestUtilities.ts | 19 ++--- utils/TestUtils.ts | 29 ++++++- utils/package.json | 2 + 7 files changed, 115 insertions(+), 100 deletions(-) diff --git a/node/package.json b/node/package.json index 21a02e70b9..16dd9851b4 100644 --- a/node/package.json +++ b/node/package.json @@ -60,7 +60,8 @@ "replace": "^1.2.2", "ts-jest": "^28.0.8", "typescript": "^4.9.5", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "semver": "^7.6.3" }, "author": "Amazon Web Services", "license": "Apache-2.0", diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 853a8aa769..e7f90d66d7 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -22,7 +22,7 @@ import { import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; import { command_request } from "../src/ProtobufMessage"; -import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; +import { runBaseTests } from "./SharedTests"; import { checkSimple, convertStringArrayToBuffer, @@ -164,7 +164,10 @@ describe("GlideClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new Transaction(); - const expectedRes = await transactionTest(transaction); + const expectedRes = await transactionTest( + transaction, + cluster.getVersion(), + ); transaction.select(0); const result = await client.exec(transaction); expectedRes.push("OK"); @@ -369,7 +372,7 @@ describe("GlideClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "function load test_%p", async (protocol) => { - if (await checkIfServerVersionLessThan("7.0.0")) return; + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = await GlideClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), @@ -469,7 +472,7 @@ describe("GlideClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "function flush test_%p", async (protocol) => { - if (await checkIfServerVersionLessThan("7.0.0")) return; + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = await GlideClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), @@ -528,7 +531,7 @@ describe("GlideClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "function delete test_%p", async (protocol) => { - if (await checkIfServerVersionLessThan("7.0.0")) return; + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = await GlideClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), @@ -667,7 +670,7 @@ describe("GlideClient", () => { options.clientName = clientName; testsFailed += 1; client = await GlideClient.createClient(options); - return { client, context: { client } }; + return { client, context: { client }, cluster }; }, close: (context: Context, testSucceeded: boolean) => { if (testSucceeded) { diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index e039c96ad5..a391dfa342 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -10,8 +10,8 @@ import { expect, it, } from "@jest/globals"; +import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; - import { ClusterClientConfiguration, ClusterTransaction, @@ -23,7 +23,7 @@ import { } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; -import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; +import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, checkSimple, @@ -83,6 +83,7 @@ describe("GlideClusterClient", () => { client, }, client, + cluster, }; }, close: (context: Context, testSucceeded: boolean) => { @@ -241,7 +242,10 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new ClusterTransaction(); - const expectedRes = await transactionTest(transaction); + const expectedRes = await transactionTest( + transaction, + cluster.getVersion(), + ); const result = await client.exec(transaction); expect(intoString(result)).toEqual(intoString(expectedRes)); }, @@ -298,9 +302,6 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); - const versionLessThan7 = - await checkIfServerVersionLessThan("7.0.0"); - const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), @@ -308,23 +309,27 @@ 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.zdiff(["abc", "zxy", "lkn"]), - client.zdiffWithScores(["abc", "zxy", "lkn"]), - client.zdiffstore("abc", ["zxy", "lkn"]), client.sunionstore("abc", ["zxy", "lkn"]), client.sunion(["abc", "zxy", "lkn"]), client.pfcount(["abc", "zxy", "lkn"]), client.sdiff(["abc", "zxy", "lkn"]), client.sdiffstore("abc", ["zxy", "lkn"]), - // TODO all rest multi-key commands except ones tested below ]; - if (!versionLessThan7) { - promises.push(client.zintercard(["abc", "zxy", "lkn"])); + if (gte(cluster.getVersion(), "6.2.0")) { + promises.push( + client.zdiff(["abc", "zxy", "lkn"]), + client.zdiffWithScores(["abc", "zxy", "lkn"]), + client.zdiffstore("abc", ["zxy", "lkn"]), + ); + } + + if (gte(cluster.getVersion(), "7.0.0")) { promises.push( + client.sintercard(["abc", "zxy", "lkn"]), + client.zintercard(["abc", "zxy", "lkn"]), client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), ); } @@ -566,7 +571,7 @@ describe("GlideClusterClient", () => { it( "function load", async () => { - if (await checkIfServerVersionLessThan("7.0.0")) + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = @@ -701,7 +706,7 @@ describe("GlideClusterClient", () => { it( "function flush", async () => { - if (await checkIfServerVersionLessThan("7.0.0")) + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = @@ -810,7 +815,7 @@ describe("GlideClusterClient", () => { it( "function delete", async () => { - if (await checkIfServerVersionLessThan("7.0.0")) + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const client = @@ -897,7 +902,7 @@ describe("GlideClusterClient", () => { [true, ProtocolVersion.RESP3], [false, ProtocolVersion.RESP3], ])("simple pubsub test", async (sharded, protocol) => { - if (sharded && (await checkIfServerVersionLessThan("7.2.0"))) { + if (sharded && cluster.checkIfServerVersionLessThan("7.2.0")) { return; } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index cbcbbb6834..a4bac4a8d1 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2,8 +2,12 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +// This file contains tests common for standalone and cluster clients, it covers API defined in +// BaseClient.ts - commands which manipulate with keys. +// Each test cases has access to a client instance and, optionally, to a cluster - object, which +// represents a running server instance. See first 2 test cases as examples. + import { expect, it } from "@jest/globals"; -import { exec } from "child_process"; import { v4 as uuidv4 } from "uuid"; import { ClosingError, @@ -38,39 +42,7 @@ import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialDa import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; import { ConditionalChange } from "../build-ts/src/commands/ConditionalChange"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; - -async function getVersion(): Promise<[number, number, number]> { - const versionString = await new Promise((resolve, reject) => { - exec(`redis-server -v`, (error, stdout) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }); - }); - const version = versionString.split("v=")[1].split(" ")[0]; - const numbers = version?.split("."); - - if (numbers.length != 3) { - return [0, 0, 0]; - } - - return [parseInt(numbers[0]), parseInt(numbers[1]), parseInt(numbers[2])]; -} - -export async function checkIfServerVersionLessThan( - minVersion: string, -): Promise { - const version = await getVersion(); - const versionToCompare = - version[0].toString() + - "." + - version[1].toString() + - "." + - version[2].toString(); - return versionToCompare < minVersion; -} +import { RedisCluster } from "../../utils/TestUtils"; export type BaseClient = GlideClient | GlideClusterClient; @@ -81,6 +53,7 @@ export function runBaseTests(config: { ) => Promise<{ context: Context; client: BaseClient; + cluster: RedisCluster; }>; close: (context: Context, testSucceeded: boolean) => void; timeout?: number; @@ -92,15 +65,18 @@ export function runBaseTests(config: { }); const runTest = async ( - test: (client: BaseClient) => Promise, + test: (client: BaseClient, cluster: RedisCluster) => Promise, protocol: ProtocolVersion, clientName?: string, ) => { - const { context, client } = await config.init(protocol, clientName); + const { context, client, cluster } = await config.init( + protocol, + clientName, + ); let testSucceeded = false; try { - await test(client); + await test(client, cluster); testSucceeded = true; } finally { config.close(context, testSucceeded); @@ -110,8 +86,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `should register client library name and version_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("7.2.0")) { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("7.2.0")) { return; } @@ -1412,8 +1388,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sintercard test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("7.0.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { return; } @@ -1753,8 +1729,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `smismember test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("6.2.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { return; } @@ -1851,7 +1827,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expire, pexpire and ttl with positive timeout_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); checkSimple(await client.set(key, "foo")).toEqual("OK"); expect(await client.expire(key, 10)).toEqual(true); @@ -1859,7 +1835,7 @@ export function runBaseTests(config: { /// set command clears the timeout. checkSimple(await client.set(key, "bar")).toEqual("OK"); const versionLessThan = - await checkIfServerVersionLessThan("7.0.0"); + cluster.checkIfServerVersionLessThan("7.0.0"); if (versionLessThan) { expect(await client.pexpire(key, 10000)).toEqual(true); @@ -1897,7 +1873,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expireAt, pexpireAt and ttl with positive timeout_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); checkSimple(await client.set(key, "foo")).toEqual("OK"); expect( @@ -1908,7 +1884,7 @@ export function runBaseTests(config: { ).toEqual(true); expect(await client.ttl(key)).toBeLessThanOrEqual(10); const versionLessThan = - await checkIfServerVersionLessThan("7.0.0"); + cluster.checkIfServerVersionLessThan("7.0.0"); if (versionLessThan) { expect( @@ -2217,8 +2193,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zintercard test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("7.0.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { return; } @@ -2260,8 +2236,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zdiff test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("6.2.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { return; } @@ -2330,8 +2306,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zdiffstore test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("6.2.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { return; } @@ -2433,8 +2409,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zmscore test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("6.2.0")) { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { return; } @@ -3170,14 +3146,14 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zrank test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key1 = uuidv4(); const key2 = uuidv4(); const membersScores = { one: 1.5, two: 2, three: 3 }; expect(await client.zadd(key1, membersScores)).toEqual(3); expect(await client.zrank(key1, "one")).toEqual(0); - if (!(await checkIfServerVersionLessThan("7.2.0"))) { + if (!cluster.checkIfServerVersionLessThan("7.2.0")) { expect(await client.zrankWithScore(key1, "one")).toEqual([ 0, 1.5, ]); @@ -3205,14 +3181,14 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zrevrank test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); const nonSetKey = uuidv4(); const membersScores = { one: 1.5, two: 2, three: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); expect(await client.zrevrank(key, "three")).toEqual(0); - if (!(await checkIfServerVersionLessThan("7.2.0"))) { + if (!cluster.checkIfServerVersionLessThan("7.2.0")) { expect(await client.zrevrankWithScore(key, "one")).toEqual([ 2, 1.5, ]); @@ -3906,7 +3882,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "object encoding test_%p", async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const string_key = uuidv4(); const list_key = uuidv4(); const hashtable_key = uuidv4(); @@ -3919,9 +3895,9 @@ export function runBaseTests(config: { const stream_key = uuidv4(); const non_existing_key = uuidv4(); const versionLessThan7 = - await checkIfServerVersionLessThan("7.0.0"); + cluster.checkIfServerVersionLessThan("7.0.0"); const versionLessThan72 = - await checkIfServerVersionLessThan("7.2.0"); + cluster.checkIfServerVersionLessThan("7.2.0"); expect(await client.objectEncoding(non_existing_key)).toEqual( null, @@ -4353,7 +4329,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `bitcount test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key1 = uuidv4(); const key2 = uuidv4(); const value = "foobar"; @@ -4383,7 +4359,7 @@ export function runBaseTests(config: { client.bitcount(key2, new BitOffsetOptions(1, 1)), ).rejects.toThrow(RequestError); - if (await checkIfServerVersionLessThan("7.0.0")) { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { await expect( client.bitcount( key1, @@ -4570,8 +4546,8 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zmpop test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - if (await checkIfServerVersionLessThan("7.0.0")) return; + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); const nonExistingKey = "{key}-0" + uuidv4(); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4a009cef85..2003a99361 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -6,6 +6,7 @@ import { beforeAll, expect } from "@jest/globals"; import { exec } from "child_process"; import parseArgs from "minimist"; import { v4 as uuidv4 } from "uuid"; +import { gte } from "semver"; import { BaseClient, BaseClientConfiguration, @@ -27,7 +28,6 @@ import { import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; -import { checkIfServerVersionLessThan } from "./SharedTests"; beforeAll(() => { Logger.init("info"); @@ -341,6 +341,7 @@ export function compareMaps( export async function transactionTest( baseTransaction: Transaction | ClusterTransaction, + version: string, ): Promise { const key1 = "{key}" + uuidv4(); const key2 = "{key}" + uuidv4(); @@ -484,7 +485,7 @@ export async function transactionTest( baseTransaction.sinter([key7, key7]); args.push(new Set(["bar", "foo"])); - if (!(await checkIfServerVersionLessThan("7.0.0"))) { + if (gte(version, "7.0.0")) { baseTransaction.sintercard([key7, key7]); args.push(2); baseTransaction.sintercard([key7, key7], 1); @@ -504,7 +505,7 @@ export async function transactionTest( baseTransaction.sismember(key7, "bar"); args.push(true); - if (!(await checkIfServerVersionLessThan("6.2.0"))) { + if (gte("6.2.0", version)) { baseTransaction.smismember(key7, ["bar", "foo", "baz"]); args.push([true, true, false]); } @@ -530,7 +531,7 @@ export async function transactionTest( baseTransaction.zrank(key8, "member1"); args.push(0); - if (!(await checkIfServerVersionLessThan("7.2.0"))) { + if (gte("7.2.0", version)) { baseTransaction.zrankWithScore(key8, "member1"); args.push([0, 1]); } @@ -538,7 +539,7 @@ export async function transactionTest( baseTransaction.zrevrank(key8, "member5"); args.push(0); - if (!(await checkIfServerVersionLessThan("7.2.0"))) { + if (gte("7.2.0", version)) { baseTransaction.zrevrankWithScore(key8, "member5"); args.push([0, 5]); } @@ -560,7 +561,7 @@ export async function transactionTest( baseTransaction.zadd(key13, { one: 1, two: 2, three: 3.5 }); args.push(3); - if (!(await checkIfServerVersionLessThan("6.2.0"))) { + if (gte("6.2.0", version)) { baseTransaction.zdiff([key13, key12]); args.push(["three"]); baseTransaction.zdiffWithScores([key13, key12]); @@ -588,7 +589,7 @@ export async function transactionTest( ); args.push(1); // key8 is now empty - if (!(await checkIfServerVersionLessThan("7.0.0"))) { + if (gte("7.0.0", version)) { baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); args.push(2); baseTransaction.zintercard([key8, key14]); @@ -648,7 +649,7 @@ export async function transactionTest( baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); args.push(6); - if (!(await checkIfServerVersionLessThan("7.0.0"))) { + if (gte("7.0.0", version)) { baseTransaction.bitcount( key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT), @@ -688,7 +689,7 @@ export async function transactionTest( true, ); - if (!(await checkIfServerVersionLessThan("7.0.0"))) { + if (gte("7.0.0", version)) { baseTransaction.functionLoad(code); args.push(libName); baseTransaction.functionLoad(code, true); diff --git a/utils/TestUtils.ts b/utils/TestUtils.ts index 9df26d9d98..e2f0f79d76 100644 --- a/utils/TestUtils.ts +++ b/utils/TestUtils.ts @@ -1,5 +1,11 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { exec, execFile } from "child_process"; +import { lt } from "semver"; + const PY_SCRIPT_PATH = __dirname + "/cluster_manager.py"; -import { execFile } from "child_process"; function parseOutput(input: string): { clusterFolder: string; @@ -32,10 +38,23 @@ function parseOutput(input: string): { export class RedisCluster { private addresses: [string, number][]; private clusterFolder: string | undefined; + private version: string; private constructor(addresses: [string, number][], clusterFolder?: string) { this.addresses = addresses; this.clusterFolder = clusterFolder; + this.version = RedisCluster.detectVersion(); + } + + private static detectVersion(): string { + exec(`redis-server -v`, (error, stdout) => { + if (error) { + throw error; + } else { + return stdout.split("v=")[1].split(" ")[0]; + } + }); + return "0.0.0"; // unreachable; } public static createCluster( @@ -95,6 +114,14 @@ export class RedisCluster { return this.addresses; } + public getVersion(): string { + return this.version; + } + + public checkIfServerVersionLessThan(minVersion: string): boolean { + return lt(minVersion, this.version); + } + public async close() { if (this.clusterFolder) { await new Promise((resolve, reject) => { diff --git a/utils/package.json b/utils/package.json index 1d7c771a8a..0bbd5c9d5b 100644 --- a/utils/package.json +++ b/utils/package.json @@ -13,10 +13,12 @@ "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.12.12", + "@types/semver": "^7.5.8", "prettier": "^2.8.8" }, "dependencies": { "child_process": "^1.0.2", + "semver": "^7.6.3", "typescript": "^5.4.5" } } From 79df75901ff7e7b4c67f1fd15fc66c51e110cd4c Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 24 Jul 2024 14:57:02 -0700 Subject: [PATCH 046/236] Addressed review comments Signed-off-by: Yi-Pin Chen --- node/src/BaseClient.ts | 16 +++++------ node/src/Commands.ts | 40 ++++++--------------------- node/src/GlideClusterClient.ts | 20 +++++++------- node/src/Transaction.ts | 8 +++--- node/tests/RedisClient.test.ts | 14 +++------- node/tests/RedisClusterClient.test.ts | 8 +++--- node/tests/TestUtilities.ts | 4 +-- 7 files changed, 40 insertions(+), 70 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d5d43709e3..2f3b00c40e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -3342,20 +3342,20 @@ export class BaseClient { * @remarks When in cluster mode, all `keys` must map to the same hash slot. * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, - * all names of keys that a function accesses must be explicitly provided as `keys`. + * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * @returns The invoked function's return value. * * @example * ```typescript - * const response = await client.fcall("Deep_Thought"); + * const response = await client.fcall("Deep_Thought", [], []); * console.log(response); // Output: Returns the function's return value. * ``` */ public fcall( func: string, - keys?: string[], - args?: string[], + keys: string[], + args: string[], ): Promise { return this.createWritePromise(createFCall(func, keys, args)); } @@ -3370,7 +3370,7 @@ export class BaseClient { * @remarks When in cluster mode, all `keys` must map to the same hash slot. * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, - * all names of keys that a function accesses must be explicitly provided as `keys`. + * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * @returns The invoked function's return value. * @@ -3378,13 +3378,13 @@ export class BaseClient { * ```typescript * const response = await client.fcallReadOnly("Deep_Thought", ["key1"], ["Answer", "to", "the", * "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"]); - * console.log(response); // Output: 42 # The return value on the function that was execute. + * console.log(response); // Output: 42 # The return value on the function that was executed. * ``` */ public fcallReadonly( func: string, - keys?: string[], - args?: string[], + keys: string[], + args: string[], ): Promise { return this.createWritePromise(createFCallReadOnly(func, keys, args)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index fe0b2fa50c..4b6fcbb73d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1589,23 +1589,11 @@ export function createBLPop( */ export function createFCall( func: string, - keys?: string[], - args?: string[], + keys: string[], + args: string[], ): command_request.Command { - const params: string[] = []; - params.push(func); - - if (keys !== undefined) { - params.push(keys.length.toString()); - params.push(...keys); - } else { - params.push("0"); - } - - if (args !== undefined) { - params.push(...args); - } - + let params: string[] = []; + params = params.concat(func, keys.length.toString(), keys, args); return createCommand(RequestType.FCall, params); } @@ -1614,23 +1602,11 @@ export function createFCall( */ export function createFCallReadOnly( func: string, - keys?: string[], - args?: string[], + keys: string[], + args: string[], ): command_request.Command { - const params: string[] = []; - params.push(func); - - if (keys !== undefined) { - params.push(keys.length.toString()); - params.push(...keys); - } else { - params.push("0"); - } - - if (args !== undefined) { - params.push(...args); - } - + let params: string[] = []; + params = params.concat(func, keys.length.toString(), keys, args); return createCommand(RequestType.FCallReadOnly, params); } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c7819b5064..0cddacdb0f 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -671,22 +671,22 @@ export class GlideClusterClient extends BaseClient { * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. * @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`. + * case the client will route the command to the nodes defined by `route`. * @returns The invoked function's return value. * * @example * ```typescript - * const response = await client.fcallRoute("Deep_Thought", undefined, "randomNode"); + * const response = await client.fcallWithRoute("Deep_Thought", [], "randomNode"); * console.log(response); // Output: Returns the function's return value. * ``` */ - public fcallRoute( + public fcallWithRoute( func: string, - args?: string[], + args: string[], route?: Routes, ): Promise { return this.createWritePromise( - createFCall(func, undefined, args), + createFCall(func, [], args), toProtobufRoute(route), ); } @@ -701,23 +701,23 @@ export class GlideClusterClient extends BaseClient { * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. * @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`. + * case the client will route the command to the nodes defined by `route`. * @returns The invoked function's return value. * * @example * ```typescript - * const response = await client.fcallReadonlyRoute("Deep_Thought", ["Answer", "to", "the", "Ultimate", + * const response = await client.fcallReadonlyWithRoute("Deep_Thought", ["Answer", "to", "the", "Ultimate", * "Question", "of", "Life,", "the", "Universe,", "and", "Everything"], "randomNode"); * console.log(response); // Output: 42 # The return value on the function that was execute. * ``` */ - public fcallReadonlyRoute( + public fcallReadonlyWithRoute( func: string, - args?: string[], + args: string[], route?: Routes, ): Promise { return this.createWritePromise( - createFCallReadOnly(func, undefined, args), + createFCallReadOnly(func, [], args), toProtobufRoute(route), ); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index db48e03b9d..2cfdaae4dc 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1865,12 +1865,12 @@ export class BaseTransaction> { * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, - * all names of keys that a function accesses must be explicitly provided as `keys`. + * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * * Command Response - The invoked function's return value. */ - public fcall(func: string, keys?: string[], args?: string[]): T { + public fcall(func: string, keys: string[], args: string[]): T { return this.addAndReturn(createFCall(func, keys, args)); } @@ -1883,12 +1883,12 @@ export class BaseTransaction> { * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, - * all names of keys that a function accesses must be explicitly provided as `keys`. + * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. * * Command Response - The invoked function's return value. */ - public fcallReadonly(func: string, keys?: string[], args?: string[]): T { + public fcallReadonly(func: string, keys: string[], args: string[]): T { return this.addAndReturn(createFCallReadOnly(func, keys, args)); } diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index fe0b78e73f..3a6a83c117 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -397,13 +397,10 @@ describe("GlideClient", () => { checkSimple(await client.functionLoad(code)).toEqual(libName); checkSimple( - await client.fcall(funcName, undefined, ["one", "two"]), + await client.fcall(funcName, [], ["one", "two"]), ).toEqual("one"); checkSimple( - await client.fcallReadonly(funcName, undefined, [ - "one", - "two", - ]), + await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); // TODO verify with FUNCTION LIST @@ -433,13 +430,10 @@ describe("GlideClient", () => { ); checkSimple( - await client.fcall(func2Name, undefined, ["one", "two"]), + await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); checkSimple( - await client.fcallReadonly(func2Name, undefined, [ - "one", - "two", - ]), + await client.fcallReadonly(func2Name, [], ["one", "two"]), ).toEqual(2); } finally { expect(await client.functionFlush()).toEqual("OK"); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index fb170a8fbb..064b155a28 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -610,7 +610,7 @@ describe("GlideClusterClient", () => { await client.functionLoad(code), ).toEqual(libName); // call functions from that library to confirm that it works - let fcall = await client.fcallRoute( + let fcall = await client.fcallWithRoute( funcName, ["one", "two"], route, @@ -621,7 +621,7 @@ describe("GlideClusterClient", () => { (value) => checkSimple(value).toEqual("one"), ); - fcall = await client.fcallReadonlyRoute( + fcall = await client.fcallReadonlyWithRoute( funcName, ["one", "two"], route, @@ -660,7 +660,7 @@ describe("GlideClusterClient", () => { await client.functionLoad(newCode, true), ).toEqual(libName); - fcall = await client.fcallRoute( + fcall = await client.fcallWithRoute( func2Name, ["one", "two"], route, @@ -671,7 +671,7 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual(2), ); - fcall = await client.fcallReadonlyRoute( + fcall = await client.fcallReadonlyWithRoute( func2Name, ["one", "two"], route, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e350bef201..03978c0cb7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -693,9 +693,9 @@ export async function transactionTest( args.push(libName); baseTransaction.functionLoad(code, true); args.push(libName); - baseTransaction.fcall(funcName, undefined, ["one", "two"]); + baseTransaction.fcall(funcName, [], ["one", "two"]); args.push("one"); - baseTransaction.fcallReadonly(funcName, undefined, ["one", "two"]); + baseTransaction.fcallReadonly(funcName, [], ["one", "two"]); args.push("one"); baseTransaction.functionDelete(libName); args.push("OK"); From b9824c2a844bd6b2ee1c7e255045ea0ed5140126 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:57:03 -0700 Subject: [PATCH 047/236] Node: add BITOP command (#2012) --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 35 +++++++ node/src/Commands.ts | 22 +++++ node/src/Transaction.ts | 22 +++++ node/tests/RedisClusterClient.test.ts | 2 + node/tests/SharedTests.ts | 126 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 9 ++ 8 files changed, 219 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fed2211b..05012ad626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) * Node: Added GETBIT command ([#1989](https://github.com/valkey-io/valkey-glide/pull/1989)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) * Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index d4b45fe370..c74dc3bc4c 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -76,6 +76,7 @@ function initialize() { const { BitOffsetOptions, BitmapIndexType, + BitwiseOperation, ConditionalChange, GeoAddOptions, GeospatialData, @@ -130,6 +131,7 @@ function initialize() { module.exports = { BitOffsetOptions, BitmapIndexType, + BitwiseOperation, ConditionalChange, GeoAddOptions, GeospatialData, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8adeaa8552..c7caf83c75 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -12,6 +12,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BitwiseOperation, ExpireOptions, InsertPosition, KeyWeight, @@ -28,6 +29,7 @@ import { createBLPop, createBRPop, createBitCount, + createBitOp, createDecr, createDecrBy, createDel, @@ -979,6 +981,39 @@ export class BaseClient { return this.createWritePromise(createDecrBy(key, amount)); } + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the result in the + * `destination`. + * + * See https://valkey.io/commands/bitop/ for more details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param operation - The bitwise operation to perform. + * @param destination - The key that will store the resulting string. + * @param keys - The list of keys to perform the bitwise operation on. + * @returns The size of the string stored in `destination`. + * + * @example + * ```typescript + * await client.set("key1", "A"); // "A" has binary value 01000001 + * await client.set("key2", "B"); // "B" has binary value 01000010 + * const result1 = await client.bitop(BitwiseOperation.AND, "destination", ["key1", "key2"]); + * console.log(result1); // Output: 1 - The size of the resulting string stored in "destination" is 1. + * + * const result2 = await client.get("destination"); + * console.log(result2); // Output: "@" - "@" has binary value 01000000 + * ``` + */ + public bitop( + operation: BitwiseOperation, + destination: string, + keys: string[], + ): Promise { + return this.createWritePromise( + createBitOp(operation, destination, keys), + ); + } + /** * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b34e7be2b1..7c8c0a785b 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -446,6 +446,28 @@ export function createDecrBy( return createCommand(RequestType.DecrBy, [key, amount.toString()]); } +/** + * Enumeration defining the bitwise operation to use in the {@link BaseClient.bitop|bitop} command. Specifies the + * bitwise operation to perform between the passed in keys. + */ +export enum BitwiseOperation { + AND = "AND", + OR = "OR", + XOR = "XOR", + NOT = "NOT", +} + +/** + * @internal + */ +export function createBitOp( + operation: BitwiseOperation, + destination: string, + keys: string[], +): command_request.Command { + return createCommand(RequestType.BitOp, [operation, destination, ...keys]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8a9b1b61e0..c23b771e46 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { AggregationType, + BitwiseOperation, ExpireOptions, InfoOptions, InsertPosition, @@ -22,6 +23,7 @@ import { createBLPop, createBRPop, createBitCount, + createBitOp, createClientGetName, createClientId, createConfigGet, @@ -395,6 +397,26 @@ export class BaseTransaction> { return this.addAndReturn(createDecrBy(key, amount)); } + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the result in the + * `destination`. + * + * See https://valkey.io/commands/bitop/ for more details. + * + * @param operation - The bitwise operation to perform. + * @param destination - The key that will store the resulting string. + * @param keys - The list of keys to perform the bitwise operation on. + * + * Command Response - The size of the string stored in `destination`. + */ + public bitop( + operation: BitwiseOperation, + destination: string, + keys: string[], + ): T { + return this.addAndReturn(createBitOp(operation, destination, keys)); + } + /** * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index a391dfa342..076d45cf49 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -13,6 +13,7 @@ import { import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; import { + BitwiseOperation, ClusterClientConfiguration, ClusterTransaction, GlideClusterClient, @@ -306,6 +307,7 @@ describe("GlideClusterClient", () => { client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), client.brpop(["abc", "zxy", "lkn"], 0.1), + client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index a4bac4a8d1..cc215cd164 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BitwiseOperation, ClosingError, ExpireOptions, GlideClient, @@ -473,6 +474,131 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const keys = [key1, key2]; + const destination = `{key}-${uuidv4()}`; + const nonExistingKey1 = `{key}-${uuidv4()}`; + const nonExistingKey2 = `{key}-${uuidv4()}`; + const nonExistingKey3 = `{key}-${uuidv4()}`; + const nonExistingKeys = [ + nonExistingKey1, + nonExistingKey2, + nonExistingKey3, + ]; + const setKey = `{key}-${uuidv4()}`; + const value1 = "foobar"; + const value2 = "abcdef"; + + checkSimple(await client.set(key1, value1)).toEqual("OK"); + checkSimple(await client.set(key2, value2)).toEqual("OK"); + expect( + await client.bitop(BitwiseOperation.AND, destination, keys), + ).toEqual(6); + checkSimple(await client.get(destination)).toEqual("`bc`ab"); + expect( + await client.bitop(BitwiseOperation.OR, destination, keys), + ).toEqual(6); + checkSimple(await client.get(destination)).toEqual("goofev"); + + // reset values for simplicity of results in XOR + checkSimple(await client.set(key1, "a")).toEqual("OK"); + checkSimple(await client.set(key2, "b")).toEqual("OK"); + expect( + await client.bitop(BitwiseOperation.XOR, destination, keys), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("\u0003"); + + // test single source key + expect( + await client.bitop(BitwiseOperation.AND, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.OR, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.XOR, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("�"); + + expect(await client.setbit(key1, 0, 1)).toEqual(0); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("\u001e"); + + // stores null when all keys hold empty strings + expect( + await client.bitop( + BitwiseOperation.AND, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop( + BitwiseOperation.OR, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop( + BitwiseOperation.XOR, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + nonExistingKey1, + ]), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + + // invalid argument - source key list cannot be empty + await expect( + client.bitop(BitwiseOperation.OR, destination, []), + ).rejects.toThrow(RequestError); + + // invalid arguments - NOT cannot be passed more than 1 key + await expect( + client.bitop(BitwiseOperation.NOT, destination, keys), + ).rejects.toThrow(RequestError); + + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + // invalid argument - source key has the wrong type + await expect( + client.bitop(BitwiseOperation.AND, destination, [setKey]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `getbit test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2003a99361..d35f203c26 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,7 @@ import { gte } from "semver"; import { BaseClient, BaseClientConfiguration, + BitwiseOperation, ClusterTransaction, GeoUnit, GlideClient, @@ -361,6 +362,7 @@ export async function transactionTest( const key16 = "{key}" + uuidv4(); // list const key17 = "{key}" + uuidv4(); // bitmap const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET + const key19 = "{key}" + uuidv4(); // bitmap const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -649,6 +651,13 @@ export async function transactionTest( baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); args.push(6); + baseTransaction.set(key19, "abcdef"); + args.push("OK"); + baseTransaction.bitop(BitwiseOperation.AND, key19, [key19, key17]); + args.push(6); + baseTransaction.get(key19); + args.push("`bc`ab"); + if (gte("7.0.0", version)) { baseTransaction.bitcount( key17, From b527226dd0302359277dca9581fc59620bf0c0a2 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:43:43 +0300 Subject: [PATCH 048/236] Node: increase ubuntu wokflow run time (#2015) Signed-off-by: Shoham Elias --- .github/workflows/node.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 5981ad2007..b3253bd17d 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -54,7 +54,7 @@ jobs: test-ubuntu-latest: runs-on: ubuntu-latest needs: load-engine-matrix - timeout-minutes: 15 + timeout-minutes: 25 strategy: fail-fast: false matrix: From 5ca56b7ccfd120f300ab8d03cbb01195f5aa5f7a Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:47:57 +0300 Subject: [PATCH 049/236] Node: Adds pubsub testing (#2008) Signed-off-by: Shoham Elias --- node/src/BaseClient.ts | 19 +- node/tests/PubSub.test.ts | 3250 +++++++++++++++++++++++++ node/tests/RedisClient.test.ts | 85 +- node/tests/RedisClusterClient.test.ts | 57 - 4 files changed, 3264 insertions(+), 147 deletions(-) create mode 100644 node/tests/PubSub.test.ts diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index c93bd189cf..429115b4e8 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -3694,7 +3694,7 @@ export class BaseClient { public close(errorMessage?: string): void { this.isClosed = true; this.promiseCallbackFunctions.forEach(([, reject]) => { - reject(new ClosingError(errorMessage)); + reject(new ClosingError(errorMessage || "")); }); // Handle pubsub futures @@ -3749,10 +3749,17 @@ export class BaseClient { ): Promise { const path = await StartSocketConnection(); const socket = await this.GetSocket(path); - return await this.__createClientInternal( - options, - socket, - constructor, - ); + + try { + return await this.__createClientInternal( + options, + socket, + constructor, + ); + } catch (err) { + // Ensure socket is closed + socket.end(); + throw err; + } } } diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts new file mode 100644 index 0000000000..1ced6c74d5 --- /dev/null +++ b/node/tests/PubSub.test.ts @@ -0,0 +1,3250 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, +} from "@jest/globals"; +import { v4 as uuidv4 } from "uuid"; +import { + BaseClientConfiguration, + ClusterClientConfiguration, + ConfigurationError, + GlideClient, + GlideClientConfiguration, + GlideClusterClient, + ProtocolVersion, + PubSubMsg, + TimeoutError, +} from ".."; +import RedisCluster from "../../utils/TestUtils"; +import { flushAndCloseClient } from "./TestUtilities"; + +export type TGlideClient = GlideClient | GlideClusterClient; + +/** + * Enumeration for specifying the method of PUBSUB subscription. + */ +const MethodTesting = { + Async: 0, // Uses asynchronous subscription method. + Sync: 1, // Uses synchronous subscription method. + Callback: 2, // Uses callback-based subscription method. +}; + +const TIMEOUT = 120000; +describe("PubSub", () => { + let cmeCluster: RedisCluster; + let cmdCluster: RedisCluster; + beforeAll(async () => { + cmdCluster = await RedisCluster.createCluster(false, 1, 1); + cmeCluster = await RedisCluster.createCluster(true, 3, 1); + }, 40000); + afterEach(async () => { + await flushAndCloseClient(false, cmdCluster.getAddresses()); + await flushAndCloseClient(true, cmeCluster.getAddresses()); + }); + afterAll(async () => { + await cmeCluster.close(); + await cmdCluster.close(); + }); + + async function createClients( + clusterMode: boolean, + options: ClusterClientConfiguration | GlideClientConfiguration, + options2: ClusterClientConfiguration | GlideClientConfiguration, + pubsubSubscriptions: + | GlideClientConfiguration.PubSubSubscriptions + | ClusterClientConfiguration.PubSubSubscriptions, + pubsubSubscriptions2?: + | GlideClientConfiguration.PubSubSubscriptions + | ClusterClientConfiguration.PubSubSubscriptions, + ): Promise<[TGlideClient, TGlideClient]> { + let client: TGlideClient | undefined; + + if (clusterMode) { + try { + options.pubsubSubscriptions = pubsubSubscriptions; + client = await GlideClusterClient.createClient(options); + options2.pubsubSubscriptions = pubsubSubscriptions2; + const client2 = await GlideClusterClient.createClient(options2); + return [client, client2]; + } catch (error) { + if (client) { + client.close(); + } + + throw error; + } + } else { + try { + options.pubsubSubscriptions = pubsubSubscriptions; + client = await GlideClient.createClient(options); + options2.pubsubSubscriptions = pubsubSubscriptions2; + const client2 = await GlideClient.createClient(options2); + return [client, client2]; + } catch (error) { + if (client) { + client.close(); + } + + throw error; + } + } + } + + const getOptions = ( + clusterMode: boolean, + protocol: ProtocolVersion = ProtocolVersion.RESP3, + ): BaseClientConfiguration => { + if (clusterMode) { + return { + addresses: cmeCluster.ports().map((port) => ({ + host: "localhost", + port, + })), + protocol, + }; + } + + return { + addresses: cmdCluster.ports().map((port) => ({ + host: "localhost", + port, + })), + protocol, + }; + }; + + function decodePubSubMsg(msg: PubSubMsg | null = null) { + if (!msg) { + return { + message: "", + channel: "", + pattern: null, + }; + } + + const stringMsg = Buffer.from(msg.message).toString("utf-8"); + const stringChannel = Buffer.from(msg.channel).toString("utf-8"); + const stringPattern = msg.pattern + ? Buffer.from(msg.pattern).toString("utf-8") + : null; + + return { + message: stringMsg, + channel: stringChannel, + pattern: stringPattern, + }; + } + + async function getMessageByMethod( + method: number, + client: TGlideClient, + messages: PubSubMsg[] | null = null, + index?: number, + ) { + if (method === MethodTesting.Async) { + const pubsubMessage = await client.getPubSubMessage(); + return decodePubSubMsg(pubsubMessage); + } else if (method === MethodTesting.Sync) { + const pubsubMessage = client.tryGetPubSubMessage(); + return decodePubSubMsg(pubsubMessage); + } else { + if (messages && index !== null) { + return decodePubSubMsg(messages[index!]); + } + + throw new Error( + "Messages and index must be provided for this method.", + ); + } + } + + async function checkNoMessagesLeft( + method: number, + client: TGlideClient, + callback: PubSubMsg[] | null = [], + expectedCallbackMessagesCount = 0, + ) { + if (method === MethodTesting.Async) { + try { + // Assert there are no messages to read + await Promise.race([ + client.getPubSubMessage(), + new Promise((_, reject) => + setTimeout( + () => reject(new TimeoutError("TimeoutError")), + 3000, + ), + ), + ]); + throw new Error("Expected TimeoutError but got a message."); + } catch (error) { + if (!(error instanceof TimeoutError)) { + throw error; + } + } + } else if (method === MethodTesting.Sync) { + const message = client.tryGetPubSubMessage(); + expect(message).toBe(null); + } else { + if (callback === null) { + throw new Error("Callback must be provided."); + } + + expect(callback.length).toBe(expectedCallbackMessagesCount); + } + } + + function createPubSubSubscription( + clusterMode: boolean, + clusterChannelsAndPatterns: Partial< + Record> + >, + standaloneChannelsAndPatterns: Partial< + Record> + >, + callback?: (msg: PubSubMsg, context: PubSubMsg[]) => void, + context: PubSubMsg[] | null = null, + ) { + if (clusterMode) { + const mySubscriptions: ClusterClientConfiguration.PubSubSubscriptions = + { + channelsAndPatterns: clusterChannelsAndPatterns, + callback: callback, + context: context, + }; + return mySubscriptions; + } + + const mySubscriptions: GlideClientConfiguration.PubSubSubscriptions = { + channelsAndPatterns: standaloneChannelsAndPatterns, + callback: callback, + context: context, + }; + return mySubscriptions; + } + + async function clientCleanup( + client: TGlideClient, + clusterModeSubs?: ClusterClientConfiguration.PubSubSubscriptions, + ) { + if (client === null) { + return; + } + + if (clusterModeSubs) { + for (const [channelType, channelPatterns] of Object.entries( + clusterModeSubs.channelsAndPatterns, + )) { + let cmd; + + if ( + channelType === + ClusterClientConfiguration.PubSubChannelModes.Exact.toString() + ) { + cmd = "UNSUBSCRIBE"; + } else if ( + channelType === + ClusterClientConfiguration.PubSubChannelModes.Pattern.toString() + ) { + cmd = "PUNSUBSCRIBE"; + } else if (!cmeCluster.checkIfServerVersionLessThan("7.0.0")) { + cmd = "SUNSUBSCRIBE"; + } else { + // Disregard sharded config for versions < 7.0.0 + continue; + } + + for (const channelPattern of channelPatterns) { + await client.customCommand([cmd, channelPattern]); + } + } + } + + client.close(); + // Wait briefly to ensure closure is completed + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + function newMessage(msg: PubSubMsg, context: PubSubMsg[]): void { + context.push(msg); + } + + const testCases: [ + boolean, + (typeof MethodTesting)[keyof typeof MethodTesting], + ][] = [ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + [false, MethodTesting.Async], + [false, MethodTesting.Sync], + [false, MethodTesting.Callback], + ]; + + /** + * Tests the basic happy path for exact PUBSUB functionality. + * + * This test covers the basic PUBSUB flow using three different methods: + * Async, Sync, and Callback. It verifies that a message published to a + * specific channel is correctly received by a subscriber. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + `pubsub exact happy path test_%p%p`, + async (clusterMode, method) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient; + let publishingClient: TGlideClient; + + try { + const channel = uuidv4(); + const message = uuidv4(); + const options = getOptions(clusterMode); + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + callback, + context, + ); + [listeningClient, publishingClient] = await createClients( + clusterMode, + options, + getOptions(clusterMode), + pubSub, + ); + + const result = await publishingClient.publish(message, channel); + + if (clusterMode) { + expect(result).toEqual(1); + } + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const pubsubMessage = await getMessageByMethod( + method, + listeningClient, + context, + 0, + ); + expect(pubsubMessage.message).toEqual(message); + expect(pubsubMessage.channel).toEqual(channel); + expect(pubsubMessage.pattern).toEqual(null); + + await checkNoMessagesLeft(method, listeningClient, context, 1); + } finally { + await clientCleanup(publishingClient!); + await clientCleanup( + listeningClient!, + clusterMode ? pubSub! : undefined, + ); + } + }, + TIMEOUT, + ); + + /** + * Test the coexistence of async and sync message retrieval methods in exact PUBSUB. + * + * This test covers the scenario where messages are published to a channel + * and received using both async and sync methods to ensure that both methods + * can coexist and function correctly. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "pubsub exact happy path coexistence test_%p", + async (clusterMode) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + + try { + const channel = uuidv4(); + const message = uuidv4(); + const message2 = uuidv4(); + + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + for (const msg of [message, message2]) { + const result = await publishingClient.publish(msg, channel); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const asyncMsgRes = await listeningClient.getPubSubMessage(); + const syncMsgRes = listeningClient.tryGetPubSubMessage(); + expect(syncMsgRes).toBeTruthy(); + + const asyncMsg = decodePubSubMsg(asyncMsgRes); + const syncMsg = decodePubSubMsg(syncMsgRes); + + expect([message, message2]).toContain(asyncMsg.message); + expect(asyncMsg.channel).toEqual(channel); + expect(asyncMsg.pattern).toBeNull(); + + expect([message, message2]).toContain(syncMsg.message); + expect(syncMsg.channel).toEqual(channel); + expect(syncMsg.pattern).toBeNull(); + + expect(asyncMsg.message).not.toEqual(syncMsg.message); + + // Assert there are no messages to read + await checkNoMessagesLeft(MethodTesting.Async, listeningClient); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + await clientCleanup(publishingClient!); + await clientCleanup( + listeningClient!, + clusterMode ? pubSub! : undefined, + ); + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving messages across many channels in exact PUBSUB. + * + * This test covers the scenario where multiple channels each receive their own + * unique message. It verifies that messages are correctly published and received + * using different retrieval methods: async, sync, and callback. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub exact happy path many channels test_%p_%p", + async (clusterMode, method) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + const NUM_CHANNELS = 256; + const shardPrefix = "{same-shard}"; + + try { + // Create a map of channels to random messages with shard prefix + const channelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const channel = `${shardPrefix}${uuidv4()}`; + const message = uuidv4(); + channelsAndMessages[channel] = message; + } + + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(channelsAndMessages)), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(channelsAndMessages)), + }, + callback, + context, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries( + channelsAndMessages, + )) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + expect( + pubsubMsg.channel in channelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + channelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete channelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channelsAndMessages).length).toEqual(0); + + // Check no messages left + await checkNoMessagesLeft( + method, + listeningClient, + context, + NUM_CHANNELS, + ); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving messages across many channels in exact PUBSUB, + * ensuring coexistence of async and sync retrieval methods. + * + * This test covers scenarios where multiple channels each receive their own unique message. + * It verifies that messages are correctly published and received using both async and sync methods + * to ensure that both methods can coexist and function correctly. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "pubsub exact happy path many channels coexistence test_%p", + async (clusterMode) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + const NUM_CHANNELS = 256; + const shardPrefix = "{same-shard}"; + + try { + // Create a map of channels to random messages with shard prefix + const channelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const channel = `${shardPrefix}${uuidv4()}`; + const message = uuidv4(); + channelsAndMessages[channel] = message; + } + + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(channelsAndMessages)), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(channelsAndMessages)), + }, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries( + channelsAndMessages, + )) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly by each method + for (let index = 0; index < NUM_CHANNELS; index++) { + const method = + index % 2 === 0 + ? MethodTesting.Sync + : MethodTesting.Async; + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + ); + + expect( + pubsubMsg.channel in channelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + channelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete channelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channelsAndMessages).length).toEqual(0); + + // Assert there are no messages to read + await checkNoMessagesLeft(MethodTesting.Async, listeningClient); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Test sharded PUBSUB functionality with different message retrieval methods. + * + * This test covers the sharded PUBSUB flow using three different methods: + * Async, Sync, and Callback. It verifies that a message published to a + * specific sharded channel is correctly received by a subscriber. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "sharded pubsub test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + const channel = uuidv4(); + const message = uuidv4(); + const publishResponse = 1; + + try { + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set([channel]), + }, + {}, + callback, + context, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + const result = await ( + publishingClient as GlideClusterClient + ).publish(message, channel, true); + + expect(result).toEqual(publishResponse); + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + context, + 0, + ); + + expect(pubsubMsg.message).toEqual(message); + expect(pubsubMsg.channel).toEqual(channel); + expect(pubsubMsg.pattern).toBeNull(); + + // Assert there are no messages to read + await checkNoMessagesLeft(method, listeningClient, context, 1); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Test sharded PUBSUB with co-existence of multiple messages. + * + * This test verifies the behavior of sharded PUBSUB when multiple messages are published + * to the same sharded channel. It ensures that both async and sync methods of message retrieval + * function correctly in this scenario. + * + * It covers the scenario where messages are published to a sharded channel and received using + * both async and sync methods. This ensures that the asynchronous and synchronous message + * retrieval methods can coexist without interfering with each other and operate as expected. + */ + it( + "sharded pubsub co-existence test", + async () => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + const channel = uuidv4(); + const message = uuidv4(); + const message2 = uuidv4(); + + try { + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + true, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set([channel]), + }, + {}, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + true, + getOptions(true), + getOptions(true), + pubSub, + ); + + let result = await ( + publishingClient as GlideClusterClient + ).publish(message, channel, true); + expect(result).toEqual(1); + + result = await (publishingClient as GlideClusterClient).publish( + message2, + channel, + true, + ); + expect(result).toEqual(1); + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const asyncMsgRes = await listeningClient.getPubSubMessage(); + const syncMsgRes = listeningClient.tryGetPubSubMessage(); + expect(syncMsgRes).toBeTruthy(); + + const asyncMsg = decodePubSubMsg(asyncMsgRes); + const syncMsg = decodePubSubMsg(syncMsgRes); + + expect([message, message2]).toContain(asyncMsg.message); + expect(asyncMsg.channel).toEqual(channel); + expect(asyncMsg.pattern).toBeNull(); + + expect([message, message2]).toContain(syncMsg.message); + expect(syncMsg.channel).toEqual(channel); + expect(syncMsg.pattern).toBeNull(); + + expect(asyncMsg.message).not.toEqual(syncMsg.message); + + // Assert there are no messages to read + await checkNoMessagesLeft(MethodTesting.Async, listeningClient); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup(listeningClient, pubSub!); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Test sharded PUBSUB with multiple channels and different message retrieval methods. + * + * This test verifies the behavior of sharded PUBSUB when multiple messages are published + * across multiple sharded channels. It covers three different message retrieval methods: + * Async, Sync, and Callback. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "sharded pubsub many channels test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + const NUM_CHANNELS = 256; + const shardPrefix = "{same-shard}"; + const publishResponse = 1; + + // Create a map of channels to random messages with shard prefix + const channelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const channel = `${shardPrefix}${uuidv4()}`; + const message = uuidv4(); + channelsAndMessages[channel] = message; + } + + try { + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set(Object.keys(channelsAndMessages)), + }, + {}, + callback, + context, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries( + channelsAndMessages, + )) { + const result = await ( + publishingClient as GlideClusterClient + ).publish(message, channel, true); + expect(result).toEqual(publishResponse); + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + + expect( + pubsubMsg.channel in channelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + channelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete channelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channelsAndMessages).length).toEqual(0); + + // Assert there are no more messages to read + await checkNoMessagesLeft( + method, + listeningClient, + context, + NUM_CHANNELS, + ); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Test PUBSUB with pattern subscription using different message retrieval methods. + * + * This test verifies the behavior of PUBSUB when subscribing to a pattern and receiving + * messages using three different methods: Async, Sync, and Callback. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub pattern test_%p_%p", + async (clusterMode, method) => { + const PATTERN = `{{channel}}:*`; + const channels: Record = { + [`{{channel}}:${uuidv4()}`]: uuidv4(), + [`{{channel}}:${uuidv4()}`]: uuidv4(), + }; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + try { + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + callback, + context, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries(channels)) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly + for (let index = 0; index < 2; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + expect(pubsubMsg.channel in channels).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + channels[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(PATTERN); + delete channels[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channels).length).toEqual(0); + + // Assert there are no more messages to read + await checkNoMessagesLeft(method, listeningClient, context, 2); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests the coexistence of async and sync message retrieval methods in pattern-based PUBSUB. + * + * This test covers the scenario where messages are published to a channel that match a specified pattern + * and received using both async and sync methods to ensure that both methods + * can coexist and function correctly. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "pubsub pattern coexistence test_%p", + async (clusterMode) => { + const PATTERN = `{{channel}}:*`; + const channels: Record = { + [`{{channel}}:${uuidv4()}`]: uuidv4(), + [`{{channel}}:${uuidv4()}`]: uuidv4(), + }; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + + try { + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries(channels)) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly by each method + for (let index = 0; index < 2; index++) { + const method = + index % 2 === 0 + ? MethodTesting.Async + : MethodTesting.Sync; + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + ); + + expect(Object.keys(channels)).toContain(pubsubMsg.channel); + expect(pubsubMsg.message).toEqual( + channels[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(PATTERN); + delete channels[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channels).length).toEqual(0); + + // Assert there are no more messages to read + await checkNoMessagesLeft(MethodTesting.Async, listeningClient); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving messages across many channels in pattern-based PUBSUB. + * + * This test covers the scenario where messages are published to multiple channels that match a specified pattern + * and received. It verifies that messages are correctly published and received + * using different retrieval methods: async, sync, and callback. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub pattern many channels test_%p", + async (clusterMode, method) => { + const NUM_CHANNELS = 256; + const PATTERN = "{{channel}}:*"; + const channels: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const channel = `{{channel}}:${uuidv4()}`; + const message = uuidv4(); + channels[channel] = message; + } + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + try { + // Create PUBSUB subscription for the test + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + callback, + context, + ); + + // Create clients for listening and publishing + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to each channel + for (const [channel, message] of Object.entries(channels)) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if all messages are received correctly + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + expect(pubsubMsg.channel in channels).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + channels[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(PATTERN); + delete channels[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(channels).length).toEqual(0); + + // Assert there are no more messages to read + await checkNoMessagesLeft( + method, + listeningClient, + context, + NUM_CHANNELS, + ); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests combined exact and pattern PUBSUB with one client. + * + * This test verifies that a single client can correctly handle both exact and pattern PUBSUB + * subscriptions. It covers the following scenarios: + * - Subscribing to multiple channels with exact names and verifying message reception. + * - Subscribing to channels using a pattern and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub combined exact and pattern test_%p_%p", + async (clusterMode, method) => { + const NUM_CHANNELS = 256; + const PATTERN = "{{pattern}}:*"; + + // Create dictionaries of channels and their corresponding messages + const exactChannelsAndMessages: Record = {}; + const patternChannelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const exactChannel = `{{channel}}:${uuidv4()}`; + const patternChannel = `{{pattern}}:${uuidv4()}`; + exactChannelsAndMessages[exactChannel] = uuidv4(); + patternChannelsAndMessages[patternChannel] = uuidv4(); + } + + const allChannelsAndMessages: Record = { + ...exactChannelsAndMessages, + ...patternChannelsAndMessages, + }; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + try { + // Setup PUBSUB for exact channels and pattern + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + callback, + context, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to all channels + for (const [channel, message] of Object.entries( + allChannelsAndMessages, + )) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const length = Object.keys(allChannelsAndMessages).length; + + // Check if all messages are received correctly + for (let index = 0; index < length; index++) { + const pubsubMsg: PubSubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + const pattern = + pubsubMsg.channel in patternChannelsAndMessages + ? PATTERN + : null; + expect( + pubsubMsg.channel in allChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + allChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(pattern); + delete allChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages + expect(Object.keys(allChannelsAndMessages).length).toEqual(0); + + await checkNoMessagesLeft( + method, + listeningClient, + context, + NUM_CHANNELS * 2, + ); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests combined exact and pattern PUBSUB with multiple clients, one for each subscription. + * + * This test verifies that separate clients can correctly handle both exact and pattern PUBSUB + * subscriptions. It covers the following scenarios: + * - Subscribing to multiple channels with exact names and verifying message reception. + * - Subscribing to channels using a pattern and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * - Verifying that no messages are left unread. + * - Properly unsubscribing from all channels to avoid interference with other tests. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub combined exact and pattern multiple clients test_%p_%p", + async (clusterMode, method) => { + const NUM_CHANNELS = 256; + const PATTERN = "{{pattern}}:*"; + + // Create dictionaries of channels and their corresponding messages + const exactChannelsAndMessages: Record = {}; + const patternChannelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const exactChannel = `{{channel}}:${uuidv4()}`; + const patternChannel = `{{pattern}}:${uuidv4()}`; + exactChannelsAndMessages[exactChannel] = uuidv4(); + patternChannelsAndMessages[patternChannel] = uuidv4(); + } + + const allChannelsAndMessages = { + ...exactChannelsAndMessages, + ...patternChannelsAndMessages, + }; + + let pubSubExact: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubPattern: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClientExact: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + let listeningClientPattern: TGlideClient | null = null; + let clientDontCare: TGlideClient | null = null; + let contextExact: PubSubMsg[] | null = null; + let contextPattern: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + contextExact = []; + contextPattern = []; + callback = newMessage; + } + + try { + // Setup PUBSUB for exact channels + pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + }, + callback, + contextExact, + ); + + [listeningClientExact, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + ); + + // Setup PUBSUB for pattern channels + pubSubPattern = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + callback, + contextPattern, + ); + + [listeningClientPattern, clientDontCare] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubPattern, + ); + + // Publish messages to all channels + for (const [channel, message] of Object.entries( + allChannelsAndMessages, + )) { + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + } + + // Allow the messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + let length = Object.keys(exactChannelsAndMessages).length; + + // Verify messages for exact PUBSUB + for (let index = 0; index < length; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClientExact, + contextExact, + index, + ); + expect( + pubsubMsg.channel in exactChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + exactChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete exactChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all exact messages + expect(Object.keys(exactChannelsAndMessages).length).toEqual(0); + + length = Object.keys(patternChannelsAndMessages).length; + + // Verify messages for pattern PUBSUB + for (let index = 0; index < length; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClientPattern, + contextPattern, + index, + ); + expect( + pubsubMsg.channel in patternChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + patternChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(PATTERN); + delete patternChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all pattern messages + expect(Object.keys(patternChannelsAndMessages).length).toEqual( + 0, + ); + + // Assert no messages are left unread + await checkNoMessagesLeft( + method, + listeningClientExact, + contextExact, + NUM_CHANNELS, + ); + await checkNoMessagesLeft( + method, + listeningClientPattern, + contextPattern, + NUM_CHANNELS, + ); + } finally { + // Cleanup clients + if (listeningClientExact) { + await clientCleanup( + listeningClientExact, + clusterMode ? pubSubExact! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + + if (listeningClientPattern) { + await clientCleanup( + listeningClientPattern, + clusterMode ? pubSubPattern! : undefined, + ); + } + + if (clientDontCare) { + await clientCleanup(clientDontCare); + } + } + }, + TIMEOUT, + ); + + /** + * Tests combined exact, pattern, and sharded PUBSUB with one client. + * + * This test verifies that a single client can correctly handle exact, pattern, and sharded PUBSUB + * subscriptions. It covers the following scenarios: + * - Subscribing to multiple channels with exact names and verifying message reception. + * - Subscribing to channels using a pattern and verifying message reception. + * - Subscribing to channels using a sharded subscription and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "pubsub combined exact, pattern, and sharded test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + const NUM_CHANNELS = 256; + const PATTERN = "{{pattern}}:*"; + const SHARD_PREFIX = "{same-shard}"; + + // Create dictionaries of channels and their corresponding messages + const exactChannelsAndMessages: Record = {}; + const patternChannelsAndMessages: Record = {}; + const shardedChannelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const exactChannel = `{{channel}}:${uuidv4()}`; + const patternChannel = `{{pattern}}:${uuidv4()}`; + const shardedChannel = `${SHARD_PREFIX}:${uuidv4()}`; + exactChannelsAndMessages[exactChannel] = uuidv4(); + patternChannelsAndMessages[patternChannel] = uuidv4(); + shardedChannelsAndMessages[shardedChannel] = uuidv4(); + } + + const publishResponse = 1; + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + try { + // Setup PUBSUB for exact, pattern, and sharded channels + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set(Object.keys(shardedChannelsAndMessages)), + }, + {}, + callback, + context, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Publish messages to exact and pattern channels + for (const [channel, message] of Object.entries({ + ...exactChannelsAndMessages, + ...patternChannelsAndMessages, + })) { + const result = await publishingClient.publish( + message, + channel, + ); + expect(result).toEqual(publishResponse); + } + + // Publish sharded messages + for (const [channel, message] of Object.entries( + shardedChannelsAndMessages, + )) { + const result = await ( + publishingClient as GlideClusterClient + ).publish(message, channel, true); + expect(result).toEqual(publishResponse); + } + + // Allow messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const allChannelsAndMessages = { + ...exactChannelsAndMessages, + ...patternChannelsAndMessages, + ...shardedChannelsAndMessages, + }; + + // Check if all messages are received correctly + for (let index = 0; index < NUM_CHANNELS * 3; index++) { + const pubsubMsg: PubSubMsg = await getMessageByMethod( + method, + listeningClient, + context, + index, + ); + const pattern = + pubsubMsg.channel in patternChannelsAndMessages + ? PATTERN + : null; + expect( + pubsubMsg.channel in allChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + allChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(pattern); + delete allChannelsAndMessages[pubsubMsg.channel]; + } + + // Assert we received all messages + expect(Object.keys(allChannelsAndMessages).length).toEqual(0); + + await checkNoMessagesLeft( + method, + listeningClient, + context, + NUM_CHANNELS * 3, + ); + } finally { + // Cleanup clients + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests combined exact, pattern, and sharded PUBSUB with multiple clients, one for each subscription. + * + * This test verifies that separate clients can correctly handle exact, pattern, and sharded PUBSUB + * subscriptions. It covers the following scenarios: + * - Subscribing to multiple channels with exact names and verifying message reception. + * - Subscribing to channels using a pattern and verifying message reception. + * - Subscribing to channels using a sharded subscription and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * - Verifying that no messages are left unread. + * - Properly unsubscribing from all channels to avoid interference with other tests. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "pubsub combined exact, pattern, and sharded multi-client test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + const NUM_CHANNELS = 256; + const PATTERN = "{{pattern}}:*"; + const SHARD_PREFIX = "{same-shard}"; + + // Create dictionaries of channels and their corresponding messages + const exactChannelsAndMessages: Record = {}; + const patternChannelsAndMessages: Record = {}; + const shardedChannelsAndMessages: Record = {}; + + for (let i = 0; i < NUM_CHANNELS; i++) { + const exactChannel = `{{channel}}:${uuidv4()}`; + const patternChannel = `{{pattern}}:${uuidv4()}`; + const shardedChannel = `${SHARD_PREFIX}:${uuidv4()}`; + exactChannelsAndMessages[exactChannel] = uuidv4(); + patternChannelsAndMessages[patternChannel] = uuidv4(); + shardedChannelsAndMessages[shardedChannel] = uuidv4(); + } + + const publishResponse = 1; + let listeningClientExact: TGlideClient | null = null; + let listeningClientPattern: TGlideClient | null = null; + let listeningClientSharded: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + + let pubSubExact: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubPattern: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubSharded: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let context: PubSubMsg[] | null = null; + let callback; + + const callbackMessagesExact: PubSubMsg[] = []; + const callbackMessagesPattern: PubSubMsg[] = []; + const callbackMessagesSharded: PubSubMsg[] = []; + + if (method === MethodTesting.Callback) { + callback = newMessage; + context = callbackMessagesExact; + } + + try { + // Setup PUBSUB for exact channels + pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set(Object.keys(exactChannelsAndMessages)), + }, + {}, + callback, + context, + ); + + [listeningClientExact, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + ); + + if (method === MethodTesting.Callback) { + context = callbackMessagesPattern; + } + + // Setup PUBSUB for pattern channels + pubSubPattern = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([PATTERN]), + }, + {}, + callback, + context, + ); + + if (method === MethodTesting.Callback) { + context = callbackMessagesSharded; + } + + pubSubSharded = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set(Object.keys(shardedChannelsAndMessages)), + }, + {}, + callback, + context, + ); + + [listeningClientPattern, listeningClientSharded] = + await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubPattern, + pubSubSharded, + ); + + // Publish messages to exact and pattern channels + for (const [channel, message] of Object.entries({ + ...exactChannelsAndMessages, + ...patternChannelsAndMessages, + })) { + const result = await publishingClient.publish( + message, + channel, + ); + expect(result).toEqual(publishResponse); + } + + // Publish sharded messages to all channels + for (const [channel, message] of Object.entries( + shardedChannelsAndMessages, + )) { + const result = await ( + publishingClient as GlideClusterClient + ).publish(message, channel, true); + expect(result).toEqual(publishResponse); + } + + // Allow messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify messages for exact PUBSUB + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClientExact, + callbackMessagesExact, + index, + ); + expect( + pubsubMsg.channel in exactChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + exactChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete exactChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages for exact PUBSUB + expect(Object.keys(exactChannelsAndMessages).length).toEqual(0); + + // Verify messages for pattern PUBSUB + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClientPattern, + callbackMessagesPattern, + index, + ); + expect( + pubsubMsg.channel in patternChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + patternChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toEqual(PATTERN); + delete patternChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages for pattern PUBSUB + expect(Object.keys(patternChannelsAndMessages).length).toEqual( + 0, + ); + + // Verify messages for sharded PUBSUB + for (let index = 0; index < NUM_CHANNELS; index++) { + const pubsubMsg = await getMessageByMethod( + method, + listeningClientSharded, + callbackMessagesSharded, + index, + ); + expect( + pubsubMsg.channel in shardedChannelsAndMessages, + ).toBeTruthy(); + expect(pubsubMsg.message).toEqual( + shardedChannelsAndMessages[pubsubMsg.channel], + ); + expect(pubsubMsg.pattern).toBeNull(); + delete shardedChannelsAndMessages[pubsubMsg.channel]; + } + + // Check that we received all messages for sharded PUBSUB + expect(Object.keys(shardedChannelsAndMessages).length).toEqual( + 0, + ); + + await checkNoMessagesLeft( + method, + listeningClientExact, + callbackMessagesExact, + NUM_CHANNELS, + ); + await checkNoMessagesLeft( + method, + listeningClientPattern, + callbackMessagesPattern, + NUM_CHANNELS, + ); + await checkNoMessagesLeft( + method, + listeningClientSharded, + callbackMessagesSharded, + NUM_CHANNELS, + ); + } finally { + // Cleanup clients + if (listeningClientExact) { + await clientCleanup( + listeningClientExact, + clusterMode ? pubSubExact! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + + if (listeningClientPattern) { + await clientCleanup( + listeningClientPattern, + clusterMode ? pubSubPattern! : undefined, + ); + } + + if (listeningClientSharded) { + await clientCleanup( + listeningClientSharded, + clusterMode ? pubSubSharded! : undefined, + ); + } + } + }, + TIMEOUT, + ); + + /** + * Tests combined PUBSUB with different channel modes using the same channel name. + * One publishing client, three listening clients, one for each mode. + * + * This test verifies that separate clients can correctly handle subscriptions for exact, pattern, and sharded channels with the same name. + * It covers the following scenarios: + * - Subscribing to an exact channel and verifying message reception. + * - Subscribing to a pattern channel and verifying message reception. + * - Subscribing to a sharded channel and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * - Verifying that no messages are left unread. + * - Properly unsubscribing from all channels to avoid interference with other tests. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "pubsub combined different channels with same name test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + const CHANNEL_NAME = "same-channel-name"; + const MESSAGE_EXACT = uuidv4(); + const MESSAGE_PATTERN = uuidv4(); + const MESSAGE_SHARDED = uuidv4(); + + let listeningClientExact: TGlideClient | null = null; + let listeningClientPattern: TGlideClient | null = null; + let listeningClientSharded: TGlideClient | null = null; + let publishingClient: TGlideClient | null = null; + + let pubSubExact: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubPattern: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubSharded: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let context: PubSubMsg[] | null = null; + let callback; + + const callbackMessagesExact: PubSubMsg[] = []; + const callbackMessagesPattern: PubSubMsg[] = []; + const callbackMessagesSharded: PubSubMsg[] = []; + + if (method === MethodTesting.Callback) { + callback = newMessage; + context = callbackMessagesExact; + } + + try { + // Setup PUBSUB for exact channel + pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + context, + ); + + [listeningClientExact, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + ); + + // Setup PUBSUB for pattern channel + if (method === MethodTesting.Callback) { + context = callbackMessagesPattern; + } + + pubSubPattern = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + context, + ); + + if (method === MethodTesting.Callback) { + context = callbackMessagesSharded; + } + + pubSubSharded = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + context, + ); + + [listeningClientPattern, listeningClientSharded] = + await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubPattern, + pubSubSharded, + ); + + // Publish messages to each channel + expect( + await publishingClient.publish(MESSAGE_EXACT, CHANNEL_NAME), + ).toEqual(2); + expect( + await publishingClient.publish( + MESSAGE_PATTERN, + CHANNEL_NAME, + ), + ).toEqual(2); + expect( + await (publishingClient as GlideClusterClient).publish( + MESSAGE_SHARDED, + CHANNEL_NAME, + true, + ), + ).toEqual(1); + + // Allow messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify message for exact and pattern PUBSUB + for (const [client, callback, pattern] of [ + [listeningClientExact, callbackMessagesExact, null], + [ + listeningClientPattern, + callbackMessagesPattern, + CHANNEL_NAME, + ], + ] as [TGlideClient, PubSubMsg[], string | null][]) { + const pubsubMsg = await getMessageByMethod( + method, + client, + callback, + 0, + ); + const pubsubMsg2 = await getMessageByMethod( + method, + client, + callback, + 1, + ); + + expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg.message, + ); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg2.message, + ); + expect(pubsubMsg.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg2.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg.pattern).toEqual(pattern); + expect(pubsubMsg2.pattern).toEqual(pattern); + } + + // Verify message for sharded PUBSUB + const pubsubMsgSharded = await getMessageByMethod( + method, + listeningClientSharded, + callbackMessagesSharded, + 0, + ); + expect(pubsubMsgSharded.message).toEqual(MESSAGE_SHARDED); + expect(pubsubMsgSharded.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsgSharded.pattern).toBeNull(); + + await checkNoMessagesLeft( + method, + listeningClientExact, + callbackMessagesExact, + 2, + ); + await checkNoMessagesLeft( + method, + listeningClientPattern, + callbackMessagesPattern, + 2, + ); + await checkNoMessagesLeft( + method, + listeningClientSharded, + callbackMessagesSharded, + 1, + ); + } finally { + // Cleanup clients + if (listeningClientExact) { + await clientCleanup( + listeningClientExact, + clusterMode ? pubSubExact! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + + if (listeningClientPattern) { + await clientCleanup( + listeningClientPattern, + clusterMode ? pubSubPattern! : undefined, + ); + } + + if (listeningClientSharded) { + await clientCleanup( + listeningClientSharded, + clusterMode ? pubSubSharded! : undefined, + ); + } + } + }, + TIMEOUT, + ); + + /** + * Tests PUBSUB with two publishing clients using the same channel name. + * One client uses pattern subscription, the other uses exact. + * The clients publish messages to each other and to themselves. + * + * This test verifies that two separate clients can correctly publish to and handle subscriptions + * for exact and pattern channels with the same name. It covers the following scenarios: + * - Subscribing to an exact channel and verifying message reception. + * - Subscribing to a pattern channel and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * - Verifying that no messages are left unread. + * - Properly unsubscribing from all channels to avoid interference with other tests. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + "pubsub two publishing clients same name test_%p_%p", + async (clusterMode, method) => { + const CHANNEL_NAME = "channel-name"; + const MESSAGE_EXACT = uuidv4(); + const MESSAGE_PATTERN = uuidv4(); + + let clientExact: TGlideClient | null = null; + let clientPattern: TGlideClient | null = null; + + let pubSubExact: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubPattern: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let contextExact: PubSubMsg[] | null = null; + let contextPattern: PubSubMsg[] | null = null; + let callback; + + const callbackMessagesExact: PubSubMsg[] = []; + const callbackMessagesPattern: PubSubMsg[] = []; + + if (method === MethodTesting.Callback) { + callback = newMessage; + contextExact = callbackMessagesExact; + contextPattern = callbackMessagesPattern; + } + + try { + // Setup PUBSUB for exact channel + pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([CHANNEL_NAME]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([CHANNEL_NAME]), + }, + callback, + contextExact, + ); + + // Setup PUBSUB for pattern channels + pubSubPattern = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([CHANNEL_NAME]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([CHANNEL_NAME]), + }, + callback, + contextPattern, + ); + + [clientExact, clientPattern] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + pubSubPattern, + ); + + // Publish messages to each channel - both clients publishing + for (const msg of [MESSAGE_EXACT, MESSAGE_PATTERN]) { + const result = await clientPattern.publish( + msg, + CHANNEL_NAME, + ); + + if (clusterMode) { + expect(result).toEqual(2); + } + } + + // Allow messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify message for exact and pattern PUBSUB + for (const [client, callback, pattern] of [ + [clientExact, callbackMessagesExact, null], + [clientPattern, callbackMessagesPattern, CHANNEL_NAME], + ] as [TGlideClient, PubSubMsg[], string | null][]) { + const pubsubMsg = await getMessageByMethod( + method, + client, + callback, + 0, + ); + const pubsubMsg2 = await getMessageByMethod( + method, + client, + callback, + 1, + ); + + expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg.message, + ); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg2.message, + ); + expect(pubsubMsg.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg2.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg.pattern).toEqual(pattern); + expect(pubsubMsg2.pattern).toEqual(pattern); + } + + await checkNoMessagesLeft( + method, + clientPattern, + callbackMessagesPattern, + 2, + ); + await checkNoMessagesLeft( + method, + clientExact, + callbackMessagesExact, + 2, + ); + } finally { + // Cleanup clients + if (clientExact) { + await clientCleanup( + clientExact, + clusterMode ? pubSubExact! : undefined, + ); + } + + if (clientPattern) { + await clientCleanup( + clientPattern, + clusterMode ? pubSubPattern! : undefined, + ); + } + } + }, + TIMEOUT, + ); + + /** + * Tests PUBSUB with 3 publishing clients using the same channel name. + * One client uses pattern subscription, one uses exact, and one uses sharded. + * + * This test verifies that 3 separate clients can correctly publish to and handle subscriptions + * for exact, sharded, and pattern channels with the same name. It covers the following scenarios: + * - Subscribing to an exact channel and verifying message reception. + * - Subscribing to a pattern channel and verifying message reception. + * - Subscribing to a sharded channel and verifying message reception. + * - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + * - Verifying that no messages are left unread. + * - Properly unsubscribing from all channels to avoid interference with other tests. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each([ + [true, MethodTesting.Async], + [true, MethodTesting.Sync], + [true, MethodTesting.Callback], + ])( + "pubsub three publishing clients same name with sharded test_%p_%p", + async (clusterMode, method) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; + + const CHANNEL_NAME = "same-channel-name"; + const MESSAGE_EXACT = uuidv4(); + const MESSAGE_PATTERN = uuidv4(); + const MESSAGE_SHARDED = uuidv4(); + + let clientExact: TGlideClient | null = null; + let clientPattern: TGlideClient | null = null; + let clientSharded: TGlideClient | null = null; + let clientDontCare: TGlideClient | null = null; + + let pubSubExact: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubPattern: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSubSharded: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let contextExact: PubSubMsg[] | null = null; + let contextPattern: PubSubMsg[] | null = null; + let contextSharded: PubSubMsg[] | null = null; + let callback; + + const callbackMessagesExact: PubSubMsg[] = []; + const callbackMessagesPattern: PubSubMsg[] = []; + const callbackMessagesSharded: PubSubMsg[] = []; + + if (method === MethodTesting.Callback) { + callback = newMessage; + contextExact = callbackMessagesExact; + contextPattern = callbackMessagesPattern; + contextSharded = callbackMessagesSharded; + } + + try { + // Setup PUBSUB for exact channel + pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + contextExact, + ); + + // Setup PUBSUB for pattern channels + pubSubPattern = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Pattern]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + contextPattern, + ); + + // Setup PUBSUB for sharded channels + pubSubSharded = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Sharded]: + new Set([CHANNEL_NAME]), + }, + {}, + callback, + contextSharded, + ); + + [clientExact, clientPattern] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + pubSubPattern, + ); + + [clientSharded, clientDontCare] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubSharded, + ); + + // Publish messages to each channel - all clients publishing + const publishResponse = 2; + + expect( + await clientPattern.publish(MESSAGE_EXACT, CHANNEL_NAME), + ).toEqual(publishResponse); + + expect( + await clientSharded.publish(MESSAGE_PATTERN, CHANNEL_NAME), + ).toEqual(publishResponse); + + expect( + await (clientExact as GlideClusterClient).publish( + MESSAGE_SHARDED, + CHANNEL_NAME, + true, + ), + ).toEqual(1); + + // Allow messages to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify message for exact and pattern PUBSUB + for (const [client, callback, pattern] of [ + [clientExact, callbackMessagesExact, null], + [clientPattern, callbackMessagesPattern, CHANNEL_NAME], + ] as [TGlideClient, PubSubMsg[], string | null][]) { + const pubsubMsg = await getMessageByMethod( + method, + client, + callback, + 0, + ); + const pubsubMsg2 = await getMessageByMethod( + method, + client, + callback, + 1, + ); + + expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg.message, + ); + expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( + pubsubMsg2.message, + ); + expect(pubsubMsg.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg2.channel).toEqual(CHANNEL_NAME); + expect(pubsubMsg.pattern).toEqual(pattern); + expect(pubsubMsg2.pattern).toEqual(pattern); + } + + const shardedMsg = await getMessageByMethod( + method, + clientSharded, + callbackMessagesSharded, + 0, + ); + + expect(shardedMsg.message).toEqual(MESSAGE_SHARDED); + expect(shardedMsg.channel).toEqual(CHANNEL_NAME); + expect(shardedMsg.pattern).toBeNull(); + + await checkNoMessagesLeft( + method, + clientPattern, + callbackMessagesPattern, + 2, + ); + await checkNoMessagesLeft( + method, + clientExact, + callbackMessagesExact, + 2, + ); + await checkNoMessagesLeft( + method, + clientSharded, + callbackMessagesSharded, + 1, + ); + } finally { + // Cleanup clients + if (clientExact) { + await clientCleanup( + clientExact, + clusterMode ? pubSubExact! : undefined, + ); + } + + if (clientPattern) { + await clientCleanup( + clientPattern, + clusterMode ? pubSubPattern! : undefined, + ); + } + + if (clientSharded) { + await clientCleanup( + clientSharded, + clusterMode ? pubSubSharded! : undefined, + ); + } + + if (clientDontCare) { + await clientCleanup(clientDontCare, undefined); + } + } + }, + TIMEOUT, + ); + describe.skip("pubsub max size message test", () => { + const generateLargeMessage = (char: string, size: number): string => { + let message = ""; + + for (let i = 0; i < size; i++) { + message += char; + } + + return message; + }; + + /** + * Tests publishing and receiving maximum size messages in PUBSUB. + * + * This test verifies that very large messages (512MB - BulkString max size) can be published and received + * correctly in both cluster and standalone modes. It ensures that the PUBSUB system + * can handle maximum size messages without errors and that async and sync message + * retrieval methods can coexist and function correctly. + * + * The test covers the following scenarios: + * - Setting up PUBSUB subscription for a specific channel. + * - Publishing two maximum size messages to the channel. + * - Verifying that the messages are received correctly using both async and sync methods. + * - Ensuring that no additional messages are left after the expected messages are received. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub exact max size message_%p", + async (clusterMode) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let listeningClient: TGlideClient | undefined; + let publishingClient: TGlideClient | undefined; + + const channel = uuidv4(); + + const message = generateLargeMessage("1", 512 * 1024 * 1024); // 512MB message + const message2 = generateLargeMessage("2", 512 * 1024 * 10); + + try { + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + let result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + + result = await publishingClient.publish(message2, channel); + + if (clusterMode) { + expect(result).toEqual(1); + } + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 15000)); + + const asyncMsg = await listeningClient.getPubSubMessage(); + expect(asyncMsg.message).toEqual(Buffer.from(message)); + expect(asyncMsg.channel).toEqual(Buffer.from(channel)); + expect(asyncMsg.pattern).toBeNull(); + + const syncMsg = listeningClient.tryGetPubSubMessage(); + expect(syncMsg).not.toBeNull(); + expect(syncMsg!.message).toEqual(Buffer.from(message2)); + expect(syncMsg!.channel).toEqual(Buffer.from(channel)); + expect(syncMsg!.pattern).toBeNull(); + + // Assert there are no messages to read + await checkNoMessagesLeft( + MethodTesting.Async, + listeningClient, + ); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving maximum size messages in sharded PUBSUB. + * + * This test verifies that very large messages (512MB - BulkString max size) can be published and received + * correctly. It ensures that the PUBSUB system + * can handle maximum size messages without errors and that async and sync message + * retrieval methods can coexist and function correctly. + * + * The test covers the following scenarios: + * - Setting up PUBSUB subscription for a specific sharded channel. + * - Publishing two maximum size messages to the channel. + * - Verifying that the messages are received correctly using both async and sync methods. + * - Ensuring that no additional messages are left after the expected messages are received. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub sharded max size message_%p", + async (clusterMode) => { + if (cmeCluster.checkIfServerVersionLessThan("7.0.0")) return; + + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let listeningClient: TGlideClient | undefined; + let publishingClient: TGlideClient | undefined; + const channel = uuidv4(); + + const message = generateLargeMessage("1", 512 * 1024 * 1024); // 512MB message + const message2 = generateLargeMessage("2", 512 * 1024 * 1024); // 512MB message + + try { + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel]), + }, + {}, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + expect( + await (publishingClient as GlideClusterClient).publish( + message, + channel, + true, + ), + ).toEqual(1); + + expect( + await (publishingClient as GlideClusterClient).publish( + message2, + channel, + true, + ), + ).toEqual(1); + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 15000)); + + const asyncMsg = await listeningClient.getPubSubMessage(); + const syncMsg = listeningClient.tryGetPubSubMessage(); + expect(syncMsg).not.toBeNull(); + + expect(asyncMsg.message).toEqual(Buffer.from(message)); + expect(asyncMsg.channel).toEqual(Buffer.from(channel)); + expect(asyncMsg.pattern).toBeNull(); + + expect(syncMsg!.message).toEqual(Buffer.from(message2)); + expect(syncMsg!.channel).toEqual(Buffer.from(channel)); + expect(syncMsg!.pattern).toBeNull(); + + // Assert there are no messages to read + await checkNoMessagesLeft( + MethodTesting.Async, + listeningClient, + ); + expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + } finally { + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving maximum size messages in exact PUBSUB with callback method. + * + * This test verifies that very large messages (512MB - BulkString max size) can be published and received + * correctly in both cluster and standalone modes. It ensures that the PUBSUB system + * can handle maximum size messages without errors and that the callback message + * retrieval method works as expected. + * + * The test covers the following scenarios: + * - Setting up PUBSUB subscription for a specific channel with a callback. + * - Publishing a maximum size message to the channel. + * - Verifying that the message is received correctly using the callback method. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub exact max size message callback_%p", + async (clusterMode) => { + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let listeningClient: TGlideClient | undefined; + let publishingClient: TGlideClient | undefined; + const channel = uuidv4(); + + const message = generateLargeMessage("0", 12 * 1024 * 1024); // 12MB message + + try { + const callbackMessages: PubSubMsg[] = []; + const callback = newMessage; + + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + callback, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + const result = await publishingClient.publish( + message, + channel, + ); + + if (clusterMode) { + expect(result).toEqual(1); + } + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 15000)); + + expect(callbackMessages.length).toEqual(1); + + expect(callbackMessages[0].message).toEqual( + Buffer.from(message), + ); + expect(callbackMessages[0].channel).toEqual( + Buffer.from(channel), + ); + expect(callbackMessages[0].pattern).toBeNull(); + // Assert no messages left + expect(callbackMessages.length).toEqual(1); + } finally { + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + + /** + * Tests publishing and receiving maximum size messages in sharded PUBSUB with callback method. + * + * This test verifies that very large messages (512MB - BulkString max size) can be published and received + * correctly. It ensures that the PUBSUB system + * can handle maximum size messages without errors and that callback + * retrieval methods can coexist and function correctly. + * + * The test covers the following scenarios: + * - Setting up PUBSUB subscription for a specific sharded channel. + * - Publishing a maximum size message to the channel. + * - Verifying that the messages are received correctly using callbacl method. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub sharded max size message callback_%p", + async (clusterMode) => { + if (cmeCluster.checkIfServerVersionLessThan("7.0.0")) return; + let pubSub: + | ClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + + let listeningClient: TGlideClient | undefined; + let publishingClient: TGlideClient | undefined; + const channel = uuidv4(); + + const message = generateLargeMessage("0", 512 * 1024 * 1024); // 512MB message + + try { + const callbackMessages: PubSubMsg[] = []; + const callback = newMessage; + + pubSub = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel]), + }, + {}, + callback, + ); + + [listeningClient, publishingClient] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + expect( + await (publishingClient as GlideClusterClient).publish( + message, + channel, + true, + ), + ).toEqual(1); + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 15000)); + + expect(callbackMessages.length).toEqual(1); + + expect(callbackMessages[0].message).toEqual( + Buffer.from(message), + ); + expect(callbackMessages[0].channel).toEqual( + Buffer.from(channel), + ); + expect(callbackMessages[0].pattern).toBeNull(); + + // Assert no messages left + expect(callbackMessages.length).toEqual(1); + } finally { + if (listeningClient) { + await clientCleanup( + listeningClient, + clusterMode ? pubSub! : undefined, + ); + } + + if (publishingClient) { + await clientCleanup(publishingClient); + } + } + }, + TIMEOUT, + ); + }); + + /** + * Tests that creating a RESP2 client with PUBSUB raises a ConfigurationError. + * + * This test ensures that the system correctly prevents the creation of a PUBSUB client + * using the RESP2 protocol version, which is not supported. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub resp2 raise an error_%p", + async (clusterMode) => { + const channel = uuidv4(); + + const pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + ); + + await expect( + createClients( + clusterMode, + getOptions(clusterMode, ProtocolVersion.RESP2), + getOptions(clusterMode, ProtocolVersion.RESP2), + pubSubExact, + ), + ).rejects.toThrow(ConfigurationError); + }, + ); + + /** + * Tests that creating a PUBSUB client with context but without a callback raises a ConfigurationError. + * + * This test ensures that the system enforces the requirement of providing a callback when + * context is supplied, preventing misconfigurations. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub context with no callback raise error_%p", + async (clusterMode) => { + const channel = uuidv4(); + const context: PubSubMsg[] = []; + + const pubSubExact = createPubSubSubscription( + clusterMode, + { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel]), + }, + undefined, // No callback provided + context, + ); + + // Attempt to create clients, expecting an error + await expect( + createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSubExact, + ), + ).rejects.toThrow(ConfigurationError); + }, + TIMEOUT, + ); +}); diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index b1c8dca976..6339f62765 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -12,13 +12,7 @@ import { } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; -import { - GlideClient, - GlideClientConfiguration, - ProtocolVersion, - PubSubMsg, - Transaction, -} from ".."; +import { GlideClient, ProtocolVersion, Transaction } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; import { command_request } from "../src/ProtobufMessage"; @@ -559,83 +553,6 @@ describe("GlideClient", () => { }, ); - 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 d7980cb2de..f2cec06ae4 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -14,7 +14,6 @@ import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; import { BitwiseOperation, - ClusterClientConfiguration, ClusterTransaction, GlideClusterClient, InfoOptions, @@ -902,60 +901,4 @@ describe("GlideClusterClient", () => { ); }, ); - - it.each([ - [true, ProtocolVersion.RESP3], - [false, ProtocolVersion.RESP3], - ])("simple pubsub test", async (sharded, protocol) => { - if (sharded && cluster.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 4cfcd1773dd29cba8b360857022d90f822d82d72 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 25 Jul 2024 11:29:38 -0700 Subject: [PATCH 050/236] Node: Rework transaction IT. (#2010) * Rework transaction IT. Signed-off-by: Yury-Fridlyand --- node/tests/RedisClient.test.ts | 6 +- node/tests/RedisClusterClient.test.ts | 3 +- node/tests/TestUtilities.ts | 375 ++++++++++++++++---------- 3 files changed, 240 insertions(+), 144 deletions(-) diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 6339f62765..09ac745aeb 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -27,6 +27,7 @@ import { parseCommandLineArgs, parseEndpoints, transactionTest, + validateTransactionResponse, } from "./TestUtilities"; /* eslint-disable @typescript-eslint/no-var-requires */ @@ -164,8 +165,9 @@ describe("GlideClient", () => { ); transaction.select(0); const result = await client.exec(transaction); - expectedRes.push("OK"); - expect(intoString(result)).toEqual(intoString(expectedRes)); + expectedRes.push(["select(0)", "OK"]); + + validateTransactionResponse(result, expectedRes); }, ); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index f2cec06ae4..75a68dcdd6 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -36,6 +36,7 @@ import { parseCommandLineArgs, parseEndpoints, transactionTest, + validateTransactionResponse, } from "./TestUtilities"; type Context = { client: GlideClusterClient; @@ -247,7 +248,7 @@ describe("GlideClusterClient", () => { cluster.getVersion(), ); const result = await client.exec(transaction); - expect(intoString(result)).toEqual(intoString(expectedRes)); + validateTransactionResponse(result, expectedRes); }, TIMEOUT, ); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 0ee485370f..a230562ced 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -40,6 +40,8 @@ function intoArrayInternal(obj: any, builder: Array) { builder.push("null"); } else if (typeof obj === "string") { builder.push(obj); + } else if (typeof obj === "number") { + builder.push(obj.toPrecision(3)); } else if (obj instanceof Uint8Array) { builder.push(obj.toString()); } else if (obj instanceof Array) { @@ -340,10 +342,44 @@ export function compareMaps( return JSON.stringify(map) == JSON.stringify(map2); } +/** + * Check transaction response. + * @param response - Transaction result received from `exec` call. + * @param expectedResponseData - Expected result data from {@link transactionTest}. + */ +export function validateTransactionResponse( + response: ReturnType[] | null, + expectedResponseData: [string, ReturnType][], +) { + const failedChecks: string[] = []; + + for (let i = 0; i < expectedResponseData.length; i++) { + const [testName, expectedResponse] = expectedResponseData[i]; + + if (intoString(response?.[i]) != intoString(expectedResponse)) { + failedChecks.push( + `${testName} failed, expected <${JSON.stringify(expectedResponse)}>, actual <${JSON.stringify(response?.[i])}>`, + ); + } + } + + if (failedChecks.length > 0) { + throw new Error( + "Checks failed in transaction response:\n" + + failedChecks.join("\n"), + ); + } +} + +/** + * Populates a transaction with commands to test. + * @param baseTransaction - A transaction. + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ export async function transactionTest( baseTransaction: Transaction | ClusterTransaction, version: string, -): Promise { +): Promise<[string, ReturnType][]> { const key1 = "{key}" + uuidv4(); const key2 = "{key}" + uuidv4(); const key3 = "{key}" + uuidv4(); @@ -365,103 +401,105 @@ export async function transactionTest( const key19 = "{key}" + uuidv4(); // bitmap const field = uuidv4(); const value = uuidv4(); - const args: ReturnType[] = []; + // array of tuples - first element is test name/description, second - expected return value + const responseData: [string, ReturnType][] = []; baseTransaction.flushall(); - args.push("OK"); + responseData.push(["flushall()", "OK"]); baseTransaction.flushall(FlushMode.SYNC); - args.push("OK"); + responseData.push(["flushall(FlushMode.SYNC)", "OK"]); baseTransaction.flushdb(); - args.push("OK"); + responseData.push(["flushdb()", "OK"]); baseTransaction.flushdb(FlushMode.SYNC); - args.push("OK"); + responseData.push(["flushdb(FlushMode.SYNC)", "OK"]); baseTransaction.dbsize(); - args.push(0); + responseData.push(["dbsize()", 0]); baseTransaction.set(key1, "bar"); - args.push("OK"); + responseData.push(['set(key1, "bar")', "OK"]); baseTransaction.getdel(key1); - args.push("bar"); + responseData.push(["getdel(key1)", "bar"]); baseTransaction.set(key1, "bar"); - args.push("OK"); + responseData.push(['set(key1, "bar")', "OK"]); baseTransaction.objectEncoding(key1); - args.push("embstr"); + responseData.push(["objectEncoding(key1)", "embstr"]); baseTransaction.type(key1); - args.push("string"); + responseData.push(["type(key1)", "string"]); baseTransaction.echo(value); - args.push(value); + responseData.push(["echo(value)", value]); baseTransaction.persist(key1); - args.push(false); - baseTransaction.set(key2, "baz", { - returnOldValue: true, - }); - args.push(null); + responseData.push(["persist(key1)", false]); + baseTransaction.set(key2, "baz", { returnOldValue: true }); + responseData.push(['set(key2, "baz", { returnOldValue: true })', null]); baseTransaction.customCommand(["MGET", key1, key2]); - args.push(["bar", "baz"]); + responseData.push(['customCommand(["MGET", key1, key2])', ["bar", "baz"]]); baseTransaction.mset({ [key3]: value }); - args.push("OK"); + responseData.push(["mset({ [key3]: value })", "OK"]); baseTransaction.mget([key1, key2]); - args.push(["bar", "baz"]); + responseData.push(["mget([key1, key2])", ["bar", "baz"]]); baseTransaction.strlen(key1); - args.push(3); + responseData.push(["strlen(key1)", 3]); baseTransaction.del([key1]); - args.push(1); + responseData.push(["del([key1])", 1]); baseTransaction.hset(key4, { [field]: value }); - args.push(1); + responseData.push(["hset(key4, { [field]: value })", 1]); baseTransaction.hlen(key4); - args.push(1); + responseData.push(["hlen(key4)", 1]); baseTransaction.hsetnx(key4, field, value); - args.push(false); + responseData.push(["hsetnx(key4, field, value)", false]); baseTransaction.hvals(key4); - args.push([value]); + responseData.push(["hvals(key4)", [value]]); baseTransaction.hget(key4, field); - args.push(value); + responseData.push(["hget(key4, field)", value]); baseTransaction.hgetall(key4); - args.push({ [field]: value }); + responseData.push(["hgetall(key4)", { [field]: value }]); baseTransaction.hdel(key4, [field]); - args.push(1); + responseData.push(["hdel(key4, [field])", 1]); baseTransaction.hmget(key4, [field]); - args.push([null]); + responseData.push(["hmget(key4, [field])", [null]]); baseTransaction.hexists(key4, field); - args.push(false); + responseData.push(["hexists(key4, field)", false]); baseTransaction.lpush(key5, [ field + "1", field + "2", field + "3", field + "4", ]); - args.push(4); + responseData.push(["lpush(key5, [1, 2, 3, 4])", 4]); baseTransaction.lpop(key5); - args.push(field + "4"); + responseData.push(["lpop(key5)", field + "4"]); baseTransaction.llen(key5); - args.push(3); + responseData.push(["llen(key5)", 3]); baseTransaction.lrem(key5, 1, field + "1"); - args.push(1); + responseData.push(['lrem(key5, 1, field + "1")', 1]); baseTransaction.ltrim(key5, 0, 1); - args.push("OK"); + responseData.push(["ltrim(key5, 0, 1)", "OK"]); baseTransaction.lset(key5, 0, field + "3"); - args.push("OK"); + responseData.push(['lset(key5, 0, field + "3")', "OK"]); baseTransaction.lrange(key5, 0, -1); - args.push([field + "3", field + "2"]); + responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]); baseTransaction.lpopCount(key5, 2); - args.push([field + "3", field + "2"]); + responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); baseTransaction.linsert( key5, InsertPosition.Before, "nonExistingPivot", "element", ); - args.push(0); + responseData.push(["linsert", 0]); baseTransaction.rpush(key6, [field + "1", field + "2", field + "3"]); - args.push(3); + responseData.push([ + 'rpush(key6, [field + "1", field + "2", field + "3"])', + 3, + ]); baseTransaction.lindex(key6, 0); - args.push(field + "1"); + responseData.push(["lindex(key6, 0)", field + "1"]); baseTransaction.rpop(key6); - args.push(field + "3"); + responseData.push(["rpop(key6)", field + "3"]); baseTransaction.rpopCount(key6, 2); - args.push([field + "2", field + "1"]); + responseData.push(["rpopCount(key6, 2)", [field + "2", field + "1"]]); baseTransaction.rpushx(key15, ["_"]); // key15 is empty - args.push(0); + responseData.push(['rpushx(key15, ["_"])', 0]); baseTransaction.lpushx(key15, ["_"]); - args.push(0); + responseData.push(['lpushx(key15, ["_"])', 0]); baseTransaction.rpush(key16, [ field + "1", field + "1", @@ -469,59 +507,68 @@ export async function transactionTest( field + "3", field + "3", ]); - args.push(5); + responseData.push(["rpush(key16, [1, 1, 2, 3, 3,])", 5]); baseTransaction.lpos(key16, field + "1", new LPosOptions({ rank: 2 })); - args.push(1); + responseData.push([ + 'lpos(key16, field + "1", new LPosOptions({ rank: 2 }))', + 1, + ]); baseTransaction.lpos( key16, field + "1", new LPosOptions({ rank: 2, count: 0 }), ); - args.push([1]); + responseData.push([ + 'lpos(key16, field + "1", new LPosOptions({ rank: 2, count: 0 }))', + [1], + ]); baseTransaction.sadd(key7, ["bar", "foo"]); - args.push(2); + responseData.push(['sadd(key7, ["bar", "foo"])', 2]); baseTransaction.sunionstore(key7, [key7, key7]); - args.push(2); + responseData.push(["sunionstore(key7, [key7, key7])", 2]); baseTransaction.sunion([key7, key7]); - args.push(new Set(["bar", "foo"])); + responseData.push(["sunion([key7, key7])", new Set(["bar", "foo"])]); baseTransaction.sinter([key7, key7]); - args.push(new Set(["bar", "foo"])); + responseData.push(["sinter([key7, key7])", new Set(["bar", "foo"])]); if (gte(version, "7.0.0")) { baseTransaction.sintercard([key7, key7]); - args.push(2); + responseData.push(["sintercard([key7, key7])", 2]); baseTransaction.sintercard([key7, key7], 1); - args.push(1); + responseData.push(["sintercard([key7, key7], 1)", 1]); } baseTransaction.sinterstore(key7, [key7, key7]); - args.push(2); + responseData.push(["sinterstore(key7, [key7, key7])", 2]); baseTransaction.sdiff([key7, key7]); - args.push(new Set()); + responseData.push(["sdiff([key7, key7])", new Set()]); baseTransaction.sdiffstore(key7, [key7]); - args.push(2); + responseData.push(["sdiffstore(key7, [key7])", 2]); baseTransaction.srem(key7, ["foo"]); - args.push(1); + responseData.push(['srem(key7, ["foo"])', 1]); baseTransaction.scard(key7); - args.push(1); + responseData.push(["scard(key7)", 1]); baseTransaction.sismember(key7, "bar"); - args.push(true); + responseData.push(['sismember(key7, "bar")', true]); if (gte("6.2.0", version)) { baseTransaction.smismember(key7, ["bar", "foo", "baz"]); - args.push([true, true, false]); + responseData.push([ + 'smismember(key7, ["bar", "foo", "baz"])', + [true, true, false], + ]); } baseTransaction.smembers(key7); - args.push(new Set(["bar"])); + responseData.push(["smembers(key7)", new Set(["bar"])]); baseTransaction.spop(key7); - args.push("bar"); + responseData.push(["spop(key7)", "bar"]); baseTransaction.spopCount(key7, 2); - args.push(new Set()); + responseData.push(["spopCount(key7, 2)", new Set()]); baseTransaction.smove(key7, key7, "non_existing_member"); - args.push(false); + responseData.push(['smove(key7, key7, "non_existing_member")', false]); baseTransaction.scard(key7); - args.push(0); + responseData.push(["scard(key7)", 0]); baseTransaction.zadd(key8, { member1: 1, member2: 2, @@ -529,147 +576,181 @@ export async function transactionTest( member4: 4, member5: 5, }); - args.push(5); + responseData.push(["zadd(key8, { ... } ", 5]); baseTransaction.zrank(key8, "member1"); - args.push(0); + responseData.push(['zrank(key8, "member1")', 0]); if (gte("7.2.0", version)) { baseTransaction.zrankWithScore(key8, "member1"); - args.push([0, 1]); + responseData.push(['zrankWithScore(key8, "member1")', [0, 1]]); } baseTransaction.zrevrank(key8, "member5"); - args.push(0); + responseData.push(['zrevrank(key8, "member5")', 0]); if (gte("7.2.0", version)) { baseTransaction.zrevrankWithScore(key8, "member5"); - args.push([0, 5]); + responseData.push(['zrevrankWithScore(key8, "member5")', [0, 5]]); } baseTransaction.zaddIncr(key8, "member2", 1); - args.push(3); + responseData.push(['zaddIncr(key8, "member2", 1)', 3]); baseTransaction.zrem(key8, ["member1"]); - args.push(1); + responseData.push(['zrem(key8, ["member1"])', 1]); baseTransaction.zcard(key8); - args.push(4); + responseData.push(["zcard(key8)", 4]); + baseTransaction.zscore(key8, "member2"); - args.push(3.0); + responseData.push(['zscore(key8, "member2")', 3.0]); baseTransaction.zrange(key8, { start: 0, stop: -1 }); - args.push(["member2", "member3", "member4", "member5"]); + responseData.push([ + "zrange(key8, { start: 0, stop: -1 })", + ["member2", "member3", "member4", "member5"], + ]); baseTransaction.zrangeWithScores(key8, { start: 0, stop: -1 }); - args.push({ member2: 3, member3: 3.5, member4: 4, member5: 5 }); + responseData.push([ + "zrangeWithScores(key8, { start: 0, stop: -1 })", + { member2: 3, member3: 3.5, member4: 4, member5: 5 }, + ]); baseTransaction.zadd(key12, { one: 1, two: 2 }); - args.push(2); + responseData.push(["zadd(key12, { one: 1, two: 2 })", 2]); baseTransaction.zadd(key13, { one: 1, two: 2, three: 3.5 }); - args.push(3); + responseData.push(["zadd(key13, { one: 1, two: 2, three: 3.5 })", 3]); if (gte("6.2.0", version)) { baseTransaction.zdiff([key13, key12]); - args.push(["three"]); + responseData.push(["zdiff([key13, key12])", ["three"]]); baseTransaction.zdiffWithScores([key13, key12]); - args.push({ three: 3.5 }); + responseData.push(["zdiffWithScores([key13, key12])", { three: 3.5 }]); baseTransaction.zdiffstore(key13, [key13, key13]); - args.push(0); + responseData.push(["zdiffstore(key13, [key13, key13])", 0]); baseTransaction.zmscore(key12, ["two", "one"]); - args.push([2.0, 1.0]); + responseData.push(['zmscore(key12, ["two", "one"]', [2.0, 1.0]]); + baseTransaction.zinterstore(key12, [key12, key13]); + responseData.push(["zinterstore(key12, [key12, key13])", 0]); + } else { + baseTransaction.zinterstore(key12, [key12, key13]); + responseData.push(["zinterstore(key12, [key12, key13])", 2]); } - baseTransaction.zinterstore(key12, [key12, key13]); - args.push(2); baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); - args.push(4); + responseData.push(['zcount(key8, { value: 2 }, "positiveInfinity")', 4]); baseTransaction.zpopmin(key8); - args.push({ member2: 3.0 }); + responseData.push(["zpopmin(key8)", { member2: 3.0 }]); baseTransaction.zpopmax(key8); - args.push({ member5: 5 }); + responseData.push(["zpopmax(key8)", { member5: 5 }]); baseTransaction.zremRangeByRank(key8, 1, 1); - args.push(1); + responseData.push(["zremRangeByRank(key8, 1, 1)", 1]); baseTransaction.zremRangeByScore( key8, "negativeInfinity", "positiveInfinity", ); - args.push(1); // key8 is now empty + responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty if (gte("7.0.0", version)) { baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); - args.push(2); + responseData.push(["zadd(key14, { one: 1.0, two: 2.0 })", 2]); baseTransaction.zintercard([key8, key14]); - args.push(0); + responseData.push(["zintercard([key8, key14])", 0]); baseTransaction.zintercard([key8, key14], 1); - args.push(0); + responseData.push(["zintercard([key8, key14], 1)", 0]); baseTransaction.zmpop([key14], ScoreFilter.MAX); - args.push([key14, { two: 2.0 }]); + responseData.push(["zmpop([key14], MAX)", [key14, { two: 2.0 }]]); baseTransaction.zmpop([key14], ScoreFilter.MAX, 1); - args.push([key14, { one: 1.0 }]); + responseData.push(["zmpop([key14], MAX, 1)", [key14, { one: 1.0 }]]); } baseTransaction.xadd(key9, [["field", "value1"]], { id: "0-1" }); - args.push("0-1"); + responseData.push([ + 'xadd(key9, [["field", "value1"]], { id: "0-1" })', + "0-1", + ]); baseTransaction.xadd(key9, [["field", "value2"]], { id: "0-2" }); - args.push("0-2"); + responseData.push([ + 'xadd(key9, [["field", "value2"]], { id: "0-2" })', + "0-2", + ]); baseTransaction.xadd(key9, [["field", "value3"]], { id: "0-3" }); - args.push("0-3"); + responseData.push([ + 'xadd(key9, [["field", "value3"]], { id: "0-3" })', + "0-3", + ]); baseTransaction.xlen(key9); - args.push(3); + responseData.push(["xlen(key9)", 3]); baseTransaction.xread({ [key9]: "0-1" }); - args.push({ - [key9]: { - "0-2": [["field", "value2"]], - "0-3": [["field", "value3"]], + responseData.push([ + 'xread({ [key9]: "0-1" })', + { + [key9]: { + "0-2": [["field", "value2"]], + "0-3": [["field", "value3"]], + }, }, - }); + ]); baseTransaction.xtrim(key9, { method: "minid", threshold: "0-2", exact: true, }); - args.push(1); + responseData.push([ + 'xtrim(key9, { method: "minid", threshold: "0-2", exact: true }', + 1, + ]); baseTransaction.rename(key9, key10); - args.push("OK"); + responseData.push(["rename(key9, key10)", "OK"]); baseTransaction.exists([key10]); - args.push(1); + responseData.push(["exists([key10])", 1]); baseTransaction.renamenx(key10, key9); - args.push(true); + responseData.push(["renamenx(key10, key9)", true]); baseTransaction.exists([key9, key10]); - args.push(1); + responseData.push(["exists([key9, key10])", 1]); baseTransaction.rpush(key6, [field + "1", field + "2", field + "3"]); - args.push(3); + responseData.push([ + 'rpush(key6, [field + "1", field + "2", field + "3"])', + 3, + ]); baseTransaction.brpop([key6], 0.1); - args.push([key6, field + "3"]); + responseData.push(["brpop([key6], 0.1)", [key6, field + "3"]]); baseTransaction.blpop([key6], 0.1); - args.push([key6, field + "1"]); + responseData.push(["blpop([key6], 0.1)", [key6, field + "1"]]); baseTransaction.setbit(key17, 1, 1); - args.push(0); + responseData.push(["setbit(key17, 1, 1)", 0]); baseTransaction.getbit(key17, 1); - args.push(1); + responseData.push(["getbit(key17, 1)", 1]); baseTransaction.set(key17, "foobar"); - args.push("OK"); + responseData.push(['set(key17, "foobar")', "OK"]); baseTransaction.bitcount(key17); - args.push(26); + responseData.push(["bitcount(key17)", 26]); baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); - args.push(6); + responseData.push(["bitcount(key17, new BitOffsetOptions(1, 1))", 6]); baseTransaction.set(key19, "abcdef"); - args.push("OK"); + responseData.push(['set(key19, "abcdef")', "OK"]); baseTransaction.bitop(BitwiseOperation.AND, key19, [key19, key17]); - args.push(6); + responseData.push([ + "bitop(BitwiseOperation.AND, key19, [key19, key17])", + 6, + ]); baseTransaction.get(key19); - args.push("`bc`ab"); + responseData.push(["get(key19)", "`bc`ab"]); if (gte("7.0.0", version)) { baseTransaction.bitcount( key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT), ); - args.push(17); + responseData.push([ + "bitcount(key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT))", + 17, + ]); } baseTransaction.pfadd(key11, ["a", "b", "c"]); - args.push(1); + responseData.push(['pfadd(key11, ["a", "b", "c"])', 1]); baseTransaction.pfcount([key11]); - args.push(3); + responseData.push(["pfcount([key11])", 3]); baseTransaction.geoadd( key18, new Map([ @@ -677,18 +758,27 @@ export async function transactionTest( ["Catania", new GeospatialData(15.087269, 37.502669)], ]), ); - args.push(2); + responseData.push(["geoadd(key18, { Palermo: ..., Catania: ... })", 2]); baseTransaction.geopos(key18, ["Palermo", "Catania"]); - args.push([ - [13.36138933897018433, 38.11555639549629859], - [15.08726745843887329, 37.50266842333162032], + responseData.push([ + 'geopos(key18, ["Palermo", "Catania"])', + [ + [13.36138933897018433, 38.11555639549629859], + [15.08726745843887329, 37.50266842333162032], + ], ]); baseTransaction.geodist(key18, "Palermo", "Catania"); - args.push(166274.1516); + responseData.push(['geodist(key18, "Palermo", "Catania")', 166274.1516]); baseTransaction.geodist(key18, "Palermo", "Catania", GeoUnit.KILOMETERS); - args.push(166.2742); + responseData.push([ + 'geodist(key18, "Palermo", "Catania", GeoUnit.KILOMETERS)', + 166.2742, + ]); baseTransaction.geohash(key18, ["Palermo", "Catania", "NonExisting"]); - args.push(["sqc8b49rny0", "sqdtr74hyu0", null]); + responseData.push([ + 'geohash(key18, ["Palermo", "Catania", "NonExisting"])', + ["sqc8b49rny0", "sqdtr74hyu0", null], + ]); const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); @@ -700,22 +790,25 @@ export async function transactionTest( if (gte("7.0.0", version)) { baseTransaction.functionLoad(code); - args.push(libName); + responseData.push(["functionLoad(code)", libName]); baseTransaction.functionLoad(code, true); - args.push(libName); + responseData.push(["functionLoad(code, true)", libName]); baseTransaction.fcall(funcName, [], ["one", "two"]); - args.push("one"); + responseData.push(['fcall(funcName, [], ["one", "two"])', "one"]); baseTransaction.fcallReadonly(funcName, [], ["one", "two"]); - args.push("one"); + responseData.push([ + 'fcallReadonly(funcName, [], ["one", "two"]', + "one", + ]); baseTransaction.functionDelete(libName); - args.push("OK"); + responseData.push(["functionDelete(libName)", "OK"]); baseTransaction.functionFlush(); - args.push("OK"); + responseData.push(["functionFlush()", "OK"]); baseTransaction.functionFlush(FlushMode.ASYNC); - args.push("OK"); + responseData.push(["functionFlush(FlushMode.ASYNC)", "OK"]); baseTransaction.functionFlush(FlushMode.SYNC); - args.push("OK"); + responseData.push(["functionFlush(FlushMode.SYNC)", "OK"]); } - return args; + return responseData; } From a37fe959929e4dc3da6c993b32490f384876170e Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 25 Jul 2024 11:39:34 -0700 Subject: [PATCH 051/236] Add rust lint config for unknown cfgs. (#2016) * Add rust lint config for unknown cfgs. Signed-off-by: Yury-Fridlyand --- glide-core/Cargo.toml | 2 ++ glide-core/tests/utilities/mod.rs | 2 +- java/Cargo.toml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/glide-core/Cargo.toml b/glide-core/Cargo.toml index 1d0b7d2967..b03196c8f3 100644 --- a/glide-core/Cargo.toml +++ b/glide-core/Cargo.toml @@ -45,6 +45,8 @@ iai-callgrind = "0.9" tokio = { version = "1", features = ["rt-multi-thread"] } glide-core = { path = ".", features = ["socket-layer"] } # always enable this feature in tests. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(standalone_heartbeat)'] } [build-dependencies] protobuf-codegen = "3" diff --git a/glide-core/tests/utilities/mod.rs b/glide-core/tests/utilities/mod.rs index 05c6f1f05a..55b2ae79f7 100644 --- a/glide-core/tests/utilities/mod.rs +++ b/glide-core/tests/utilities/mod.rs @@ -193,7 +193,7 @@ impl RedisServer { // prepare redis with TLS redis_cmd .arg("--tls-port") - .arg(&port.to_string()) + .arg(port.to_string()) .arg("--port") .arg("0") .arg("--tls-cert-file") diff --git a/java/Cargo.toml b/java/Cargo.toml index 452b80856c..7c5e6798f5 100644 --- a/java/Cargo.toml +++ b/java/Cargo.toml @@ -22,3 +22,6 @@ bytes = { version = "1.6.0" } [profile.release] lto = true debug = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(ffi_test)'] } From 1c70afbeef1e640eb276c1958dc2797bf1da5728 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:38:44 -0700 Subject: [PATCH 052/236] Node: add LMOVE (#2002) * implement lmove Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 43 ++++++++++++++ node/src/Commands.ts | 31 ++++++++++ node/src/Transaction.ts | 29 ++++++++++ node/tests/SharedTests.ts | 111 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 24 +++++++- 7 files changed, 239 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633798767f..8f3d15f0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index c74dc3bc4c..12df4b5b47 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -92,6 +92,7 @@ function initialize() { PeriodicChecks, Logger, LPosOptions, + ListDirection, ExpireOptions, FlushMode, GeoUnit, @@ -147,6 +148,7 @@ function initialize() { PeriodicChecks, Logger, LPosOptions, + ListDirection, ExpireOptions, FlushMode, GeoUnit, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 429115b4e8..b7d03103f6 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -17,6 +17,7 @@ import { GeoUnit, InsertPosition, KeyWeight, + ListDirection, RangeByIndex, RangeByLex, RangeByScore, @@ -63,6 +64,7 @@ import { createLIndex, createLInsert, createLLen, + createLMove, createLPop, createLPos, createLPush, @@ -1465,6 +1467,47 @@ export class BaseClient { return this.createWritePromise(createLLen(key)); } + /** + * Atomically pops and removes the left/right-most element to the list stored at `source` + * depending on `whereTo`, and pushes the element at the first/last element of the list + * stored at `destination` depending on `whereFrom`, see {@link ListDirection}. + * + * See https://valkey.io/commands/lmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * @returns The popped element, or `null` if `source` does not exist. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.lpush("testKey1", ["two", "one"]); + * await client.lpush("testKey2", ["four", "three"]); + * + * const result1 = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT); + * console.log(result1); // Output: "one". + * + * const updated_array_key1 = await client.lrange("testKey1", 0, -1); + * console.log(updated_array); // Output: "two". + * + * const updated_array_key2 = await client.lrange("testKey2", 0, -1); + * console.log(updated_array_key2); // Output: ["one", "three", "four"]. + * ``` + */ + public async lmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + ): Promise { + return this.createWritePromise( + createLMove(source, destination, whereFrom, whereTo), + ); + } + /** * 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. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7b40948d0f..7b20ae39f4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -583,6 +583,37 @@ export function createLLen(key: string): command_request.Command { return createCommand(RequestType.LLen, [key]); } +/** + * Enumeration representing element popping or adding direction for the List Based Commands. + */ +export enum ListDirection { + /** + * Represents the option that elements should be popped from or added to the left side of a list. + */ + LEFT = "LEFT", + /** + * Represents the option that elements should be popped from or added to the right side of a list. + */ + RIGHT = "RIGHT", +} + +/** + * @internal + */ +export function createLMove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, +): command_request.Command { + return createCommand(RequestType.LMove, [ + source, + destination, + whereFrom, + whereTo, + ]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index fb56e7ff93..801b093955 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -10,6 +10,7 @@ import { InfoOptions, InsertPosition, KeyWeight, + ListDirection, LolwutOptions, RangeByIndex, RangeByLex, @@ -72,6 +73,7 @@ import { createLIndex, createLInsert, createLLen, + createLMove, createLPop, createLPos, createLPush, @@ -707,6 +709,33 @@ export class BaseTransaction> { return this.addAndReturn(createLLen(key)); } + /** + * Atomically pops and removes the left/right-most element to the list stored at `source` + * depending on `whereFrom`, and pushes the element at the first/last element of the list + * stored at `destination` depending on `whereTo`, see {@link ListDirection}. + * + * See https://valkey.io/commands/lmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * + * Command Response - The popped element, or `null` if `source` does not exist. + * + * since Valkey version 6.2.0. + */ + public lmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + ): T { + return this.addAndReturn( + createLMove(source, destination, whereFrom, whereTo), + ); + } + /** * 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. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index cc215cd164..5b74140add 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -13,6 +13,7 @@ import { BitwiseOperation, ClosingError, ExpireOptions, + ListDirection, GlideClient, GlideClusterClient, InfoOptions, @@ -1104,6 +1105,116 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lmove list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const lpushArgs1 = ["2", "1"]; + const lpushArgs2 = ["4", "3"]; + + // Initialize the tests + expect(await client.lpush(key1, lpushArgs1)).toEqual(2); + expect(await client.lpush(key2, lpushArgs2)).toEqual(2); + + // Move from LEFT to LEFT + checkSimple( + await client.lmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).toEqual("1"); + + // Move from LEFT to RIGHT + checkSimple( + await client.lmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + ), + ).toEqual("2"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + "4", + "2", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([]); + + // Move from RIGHT to LEFT - non-existing destination key + checkSimple( + await client.lmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.LEFT, + ), + ).toEqual("2"); + + // Move from RIGHT to RIGHT + checkSimple( + await client.lmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.RIGHT, + ), + ).toEqual("4"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "2", + "4", + ]); + + // Non-existing source key + expect( + await client.lmove( + "{key}-non_existing_key" + uuidv4(), + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).toEqual(null); + + // Non-list source key + const key3 = "{key}-3" + uuidv4(); + checkSimple(await client.set(key3, "value")).toEqual("OK"); + await expect( + client.lmove( + key3, + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).rejects.toThrow(RequestError); + + // Non-list destination key + await expect( + client.lmove( + key1, + key3, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lset test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a230562ced..352f9f3dab 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -17,6 +17,7 @@ import { GlideClusterClient, InsertPosition, Logger, + ListDirection, ProtocolVersion, ReturnType, ScoreFilter, @@ -399,6 +400,7 @@ export async function transactionTest( const key17 = "{key}" + uuidv4(); // bitmap const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap + const key20 = "{key}" + uuidv4(); // list const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -476,8 +478,26 @@ export async function transactionTest( responseData.push(['lset(key5, 0, field + "3")', "OK"]); baseTransaction.lrange(key5, 0, -1); responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]); - baseTransaction.lpopCount(key5, 2); - responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + + if (gte("6.2.0", version)) { + baseTransaction.lmove( + key5, + key20, + ListDirection.LEFT, + ListDirection.LEFT, + ); + responseData.push([ + "lmove(key5, key20, ListDirection.LEFT, ListDirection.LEFT)", + field + "3", + ]); + + baseTransaction.lpopCount(key5, 2); + responseData.push(["lpopCount(key5, 2)", [field + "2"]]); + } else { + baseTransaction.lpopCount(key5, 2); + responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + } + baseTransaction.linsert( key5, InsertPosition.Before, From 4e7e7c3657e88c077bcc52172c24aefc231f48be Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:06:22 -0700 Subject: [PATCH 053/236] Node: add BITPOS command (#1998) --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 +- node/src/BaseClient.ts | 79 ++++++++++++++++ node/src/Commands.ts | 41 ++++++++ node/src/Transaction.ts | 55 +++++++++++ node/src/commands/BitOffsetOptions.ts | 14 +-- node/tests/SharedTests.ts | 131 +++++++++++++++++++++++++- node/tests/TestUtilities.ts | 13 ++- 8 files changed, 315 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3d15f0a1..d39d8ac5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) +* Node: Added BITPOS command ([#1998](https://github.com/valkey-io/valkey-glide/pull/1998)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 12df4b5b47..420529e0e7 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -74,8 +74,8 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { - BitOffsetOptions, BitmapIndexType, + BitOffsetOptions, BitwiseOperation, ConditionalChange, GeoAddOptions, @@ -130,8 +130,8 @@ function initialize() { } = nativeBinding; module.exports = { - BitOffsetOptions, BitmapIndexType, + BitOffsetOptions, BitwiseOperation, ConditionalChange, GeoAddOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b7d03103f6..0808339c44 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -12,6 +12,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BitmapIndexType, BitwiseOperation, ExpireOptions, GeoUnit, @@ -32,6 +33,7 @@ import { createBRPop, createBitCount, createBitOp, + createBitPos, createDecr, createDecrBy, createDel, @@ -1062,6 +1064,83 @@ export class BaseClient { return this.createWritePromise(createSetBit(key, offset, value)); } + /** + * Returns the position of the first bit matching the given `bit` value. The optional starting offset + * `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + * the last byte of the list, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - (Optional) The starting offset. If not supplied, the search will start at the beginning of the string. + * @returns The position of the first occurrence of `bit` in the binary value of the string held at `key`. + * If `start` was provided, the search begins at the offset indicated by `start`. + * + * @example + * ```typescript + * await client.set("key1", "A1"); // "A1" has binary value 01000001 00110001 + * const result1 = await client.bitpos("key1", 1); + * console.log(result1); // Output: 1 - The first occurrence of bit value 1 in the string stored at "key1" is at the second position. + * + * const result2 = await client.bitpos("key1", 1, -1); + * console.log(result2); // Output: 10 - The first occurrence of bit value 1, starting at the last byte in the string stored at "key1", is at the eleventh position. + * ``` + */ + public async bitpos( + key: string, + bit: number, + start?: number, + ): Promise { + return this.createWritePromise(createBitPos(key, bit, start)); + } + + /** + * Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + * `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + * numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + * being the penultimate, and so on. + * + * If you are using Valkey 7.0.0 or above, the optional `indexType` can also be provided to specify whether the + * `start` and `end` offsets specify BIT or BYTE offsets. If `indexType` is not provided, BYTE offsets + * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + * specified, `start=0` and `end=2` means to look at the first three bytes. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - The starting offset. + * @param end - The ending offset. + * @param indexType - (Optional) The index offset type. This option can only be specified if you are using Valkey + * version 7.0.0 or above. Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. If no + * index type is provided, the indexes will be assumed to be byte indexes. + * @returns The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary + * value of the string held at `key`. + * + * @example + * ```typescript + * await client.set("key1", "A12"); // "A12" has binary value 01000001 00110001 00110010 + * const result1 = await client.bitposInterval("key1", 1, 1, -1); + * console.log(result1); // Output: 10 - The first occurrence of bit value 1 in the second byte to the last byte of the string stored at "key1" is at the eleventh position. + * + * const result2 = await client.bitposInterval("key1", 1, 2, 9, BitmapIndexType.BIT); + * console.log(result2); // Output: 7 - The first occurrence of bit value 1 in the third to tenth bits of the string stored at "key1" is at the eighth position. + * ``` + */ + public async bitposInterval( + key: string, + bit: number, + start: number, + end: number, + indexType?: BitmapIndexType, + ): Promise { + return this.createWritePromise( + createBitPos(key, bit, start, end, indexType), + ); + } + /** Retrieve the value associated with `field` in the hash stored at `key`. * See https://valkey.io/commands/hget/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7b20ae39f4..0a85954fde 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1706,6 +1706,47 @@ export function createBitCount( return createCommand(RequestType.BitCount, args); } +/** + * Enumeration specifying if index arguments are BYTE indexes or BIT indexes. + * Can be specified in {@link BitOffsetOptions}, which is an optional argument to the {@link BaseClient.bitcount|bitcount} command. + * Can also be specified as an optional argument to the {@link BaseClient.bitposInverval|bitposInterval} command. + * + * since - Valkey version 7.0.0. + */ +export enum BitmapIndexType { + /** Specifies that provided indexes are byte indexes. */ + BYTE = "BYTE", + /** Specifies that provided indexes are bit indexes. */ + BIT = "BIT", +} + +/** + * @internal + */ +export function createBitPos( + key: string, + bit: number, + start?: number, + end?: number, + indexType?: BitmapIndexType, +): command_request.Command { + const args = [key, bit.toString()]; + + if (start !== undefined) { + args.push(start.toString()); + } + + if (end !== undefined) { + args.push(end.toString()); + } + + if (indexType) { + args.push(indexType); + } + + return createCommand(RequestType.BitPos, args); +} + export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 801b093955..8521779848 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { AggregationType, + BitmapIndexType, BitwiseOperation, ExpireOptions, GeoUnit, @@ -26,6 +27,7 @@ import { createBRPop, createBitCount, createBitOp, + createBitPos, createClientGetName, createClientId, createConfigGet, @@ -455,6 +457,59 @@ export class BaseTransaction> { return this.addAndReturn(createSetBit(key, offset, value)); } + /** + * Returns the position of the first bit matching the given `bit` value. The optional starting offset + * `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + * the last byte of the list, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - (Optional) The starting offset. If not supplied, the search will start at the beginning of the string. + * + * Command Response - The position of the first occurrence of `bit` in the binary value of the string held at `key`. + * If `start` was provided, the search begins at the offset indicated by `start`. + */ + public bitpos(key: string, bit: number, start?: number): T { + return this.addAndReturn(createBitPos(key, bit, start)); + } + + /** + * Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + * `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + * numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + * being the penultimate, and so on. + * + * If you are using Valkey 7.0.0 or above, the optional `indexType` can also be provided to specify whether the + * `start` and `end` offsets specify BIT or BYTE offsets. If `indexType` is not provided, BYTE offsets + * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + * specified, `start=0` and `end=2` means to look at the first three bytes. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - The starting offset. + * @param end - The ending offset. + * @param indexType - (Optional) The index offset type. This option can only be specified if you are using Valkey + * version 7.0.0 or above. Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. If no + * index type is provided, the indexes will be assumed to be byte indexes. + * + * Command Response - The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the + * binary value of the string held at `key`. + */ + public bitposInterval( + key: string, + bit: number, + start: number, + end: number, + indexType?: BitmapIndexType, + ): T { + return this.addAndReturn(createBitPos(key, bit, start, end, indexType)); + } + /** Reads the configuration parameters of a running Redis server. * See https://valkey.io/commands/config-get/ for details. * diff --git a/node/src/commands/BitOffsetOptions.ts b/node/src/commands/BitOffsetOptions.ts index 64f6f8a82e..5f5d6800e6 100644 --- a/node/src/commands/BitOffsetOptions.ts +++ b/node/src/commands/BitOffsetOptions.ts @@ -5,19 +5,7 @@ // Import below added to fix up the TSdoc link, but eslint blames for unused import. /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { BaseClient } from "src/BaseClient"; - -/** - * Enumeration specifying if index arguments are BYTE indexes or BIT indexes. - * Can be specified in {@link BitOffsetOptions}, which is an optional argument to the {@link BaseClient.bitcount|bitcount} command. - * - * since - Valkey version 7.0.0. - */ -export enum BitmapIndexType { - /** Specifies that indexes provided to {@link BitOffsetOptions} are byte indexes. */ - BYTE = "BYTE", - /** Specifies that indexes provided to {@link BitOffsetOptions} are bit indexes. */ - BIT = "BIT", -} +import { BitmapIndexType } from "src/Commands"; /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5b74140add..564ce1bd6e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BitmapIndexType, BitwiseOperation, ClosingError, ExpireOptions, @@ -35,10 +36,7 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; -import { - BitmapIndexType, - BitOffsetOptions, -} from "../build-ts/src/commands/BitOffsetOptions"; +import { BitOffsetOptions } from "../build-ts/src/commands/BitOffsetOptions"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; @@ -661,6 +659,131 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitpos and bitposInterval test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; + const value = "?f0obar"; // 00111111 01100110 00110000 01101111 01100010 01100001 01110010 + + checkSimple(await client.set(key, value)).toEqual("OK"); + expect(await client.bitpos(key, 0)).toEqual(0); + expect(await client.bitpos(key, 1)).toEqual(2); + expect(await client.bitpos(key, 1, 1)).toEqual(9); + expect(await client.bitposInterval(key, 0, 3, 5)).toEqual(24); + + // -1 is returned if start > end + expect(await client.bitposInterval(key, 0, 1, 0)).toEqual(-1); + + // `BITPOS` returns -1 for non-existing strings + expect(await client.bitpos(nonExistingKey, 1)).toEqual(-1); + expect( + await client.bitposInterval(nonExistingKey, 1, 3, 5), + ).toEqual(-1); + + // invalid argument - bit value must be 0 or 1 + await expect(client.bitpos(key, 2)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitposInterval(key, 2, 3, 5), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a string + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + await expect(client.bitpos(setKey, 1)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitposInterval(setKey, 1, 1, -1), + ).rejects.toThrow(RequestError); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + await expect( + client.bitposInterval( + key, + 1, + 1, + -1, + BitmapIndexType.BYTE, + ), + ).rejects.toThrow(RequestError); + await expect( + client.bitposInterval( + key, + 1, + 1, + -1, + BitmapIndexType.BIT, + ), + ).rejects.toThrow(RequestError); + } else { + expect( + await client.bitposInterval( + key, + 0, + 3, + 5, + BitmapIndexType.BYTE, + ), + ).toEqual(24); + expect( + await client.bitposInterval( + key, + 1, + 43, + -2, + BitmapIndexType.BIT, + ), + ).toEqual(47); + expect( + await client.bitposInterval( + nonExistingKey, + 1, + 3, + 5, + BitmapIndexType.BYTE, + ), + ).toEqual(-1); + expect( + await client.bitposInterval( + nonExistingKey, + 1, + 3, + 5, + BitmapIndexType.BIT, + ), + ).toEqual(-1); + + // -1 is returned if the bit value wasn't found + expect( + await client.bitposInterval( + key, + 1, + -1, + -1, + BitmapIndexType.BIT, + ), + ).toEqual(-1); + + // key exists, but it is not a string + await expect( + client.bitposInterval( + setKey, + 1, + 1, + -1, + BitmapIndexType.BIT, + ), + ).rejects.toThrow(RequestError); + } + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set with timeout parameter_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 352f9f3dab..d158350556 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,7 @@ import { gte } from "semver"; import { BaseClient, BaseClientConfiguration, + BitmapIndexType, BitwiseOperation, ClusterTransaction, GeoUnit, @@ -23,10 +24,7 @@ import { ScoreFilter, Transaction, } from ".."; -import { - BitmapIndexType, - BitOffsetOptions, -} from "../build-ts/src/commands/BitOffsetOptions"; +import { BitOffsetOptions } from "../build-ts/src/commands/BitOffsetOptions"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; @@ -745,6 +743,8 @@ export async function transactionTest( responseData.push(["bitcount(key17)", 26]); baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); responseData.push(["bitcount(key17, new BitOffsetOptions(1, 1))", 6]); + baseTransaction.bitpos(key17, 1); + responseData.push(["bitpos(key17, 1)", 1]); baseTransaction.set(key19, "abcdef"); responseData.push(['set(key19, "abcdef")', "OK"]); @@ -765,6 +765,11 @@ export async function transactionTest( "bitcount(key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT))", 17, ]); + baseTransaction.bitposInterval(key17, 1, 44, 50, BitmapIndexType.BIT); + responseData.push([ + "bitposInterval(key17, 1, 44, 50, BitmapIndexType.BIT)", + 46, + ]); } baseTransaction.pfadd(key11, ["a", "b", "c"]); From b778d3509fd626026bacb614d5310cb1654ff979 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 25 Jul 2024 15:26:07 -0700 Subject: [PATCH 054/236] Node: Add `ZINCRBY` command (#2009) * Node: Add ZINCRBY command --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 34 ++++++++++++++++++++++ node/src/Commands.ts | 15 ++++++++++ node/src/Transaction.ts | 18 ++++++++++++ node/tests/SharedTests.ts | 33 +++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ python/python/glide/async_commands/core.py | 4 +-- 7 files changed, 105 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d39d8ac5ba..857f9725ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) * Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) +* Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) #### Fixes * Java: Add overloads for XADD to allow duplicate entry keys ([#1970](https://github.com/valkey-io/valkey-glide/pull/1970)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0808339c44..ac62c35bcd 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -123,6 +123,7 @@ import { createZDiff, createZDiffStore, createZDiffWithScores, + createZIncrBy, createZInterCard, createZInterstore, createZMPop, @@ -3695,6 +3696,39 @@ export class BaseClient { return this.createWritePromise(createZMPop(key, modifier, count)); } + /** + * Increments the score of `member` in the sorted set stored at `key` by `increment`. + * If `member` does not exist in the sorted set, it is added with `increment` as its score. + * If `key` does not exist, a new sorted set is created with the specified member as its sole member. + * + * See https://valkey.io/commands/zincrby/ for details. + * + * @param key - The key of the sorted set. + * @param increment - The score increment. + * @param member - A member of the sorted set. + * + * @returns The new score of `member`. + * + * @example + * ```typescript + * // Example usage of zincrBy method to increment the value of a member's score + * await client.zadd("my_sorted_set", {"member": 10.5, "member2": 8.2}); + * console.log(await client.zincrby("my_sorted_set", 1.2, "member")); + * // Output: 11.7 - The member existed in the set before score was altered, the new score is 11.7. + * console.log(await client.zincrby("my_sorted_set", -1.7, "member")); + * // Output: 10.0 - Negative increment, decrements the score. + * console.log(await client.zincrby("my_sorted_set", 5.5, "non_existing_member")); + * // Output: 5.5 - A new member is added to the sorted set with the score of 5.5. + * ``` + */ + public async zincrby( + key: string, + increment: number, + member: string, + ): Promise { + return this.createWritePromise(createZIncrBy(key, increment, member)); + } + /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0a85954fde..044ddc9759 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2082,3 +2082,18 @@ export function createZMPop( return createCommand(RequestType.ZMPop, args); } + +/** + * @internal + */ +export function createZIncrBy( + key: string, + increment: number, + member: string, +): command_request.Command { + return createCommand(RequestType.ZIncrBy, [ + key, + increment.toString(), + member, + ]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8521779848..9c4aaeb347 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -151,6 +151,7 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + createZIncrBy, } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; @@ -2181,6 +2182,23 @@ export class BaseTransaction> { return this.addAndReturn(createZMPop(keys, modifier, count)); } + /** + * Increments the score of `member` in the sorted set stored at `key` by `increment`. + * If `member` does not exist in the sorted set, it is added with `increment` as its score. + * If `key` does not exist, a new sorted set is created with the specified member as its sole member. + * + * See https://valkey.io/commands/zincrby/ for details. + * + * @param key - The key of the sorted set. + * @param increment - The score increment. + * @param member - A member of the sorted set. + * + * Command Response - The new score of `member`. + */ + public zincrby(key: string, increment: number, member: string): T { + return this.addAndReturn(createZIncrBy(key, increment, member)); + } + /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 564ce1bd6e..e0bd789c40 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4968,6 +4968,39 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zincrby test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = "{key}" + uuidv4(); + const member = "{member}-1" + uuidv4(); + const othermember = "{member}-1" + uuidv4(); + const stringKey = "{key}-string" + uuidv4(); + + // key does not exist + expect(await client.zincrby(key, 2.5, member)).toEqual(2.5); + expect(await client.zscore(key, member)).toEqual(2.5); + + // key exists, but value doesn't + expect(await client.zincrby(key, -3.3, othermember)).toEqual( + -3.3, + ); + expect(await client.zscore(key, othermember)).toEqual(-3.3); + + // updating existing value in existing key + expect(await client.zincrby(key, 1.0, member)).toEqual(3.5); + expect(await client.zscore(key, member)).toEqual(3.5); + + // Key exists, but it is not a sorted set + expect(await client.set(stringKey, "value")).toEqual("OK"); + await expect( + client.zincrby(stringKey, 0.5, "_"), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `geodist test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d158350556..fbe9722921 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -613,6 +613,8 @@ export async function transactionTest( baseTransaction.zaddIncr(key8, "member2", 1); responseData.push(['zaddIncr(key8, "member2", 1)', 3]); + baseTransaction.zincrby(key8, 0.3, "member1"); + responseData.push(['zincrby(key8, 0.3, "member1")', 1.3]); baseTransaction.zrem(key8, ["member1"]); responseData.push(['zrem(key8, ["member1"])', 1]); baseTransaction.zcard(key8); diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 3589f8d340..a267c73d25 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -4202,9 +4202,9 @@ async def zincrby( >>> await client.zincrby("my_sorted_set", 1.2, "member") 11.7 # The member existed in the set before score was altered, the new score is 11.7. >>> await client.zincrby("my_sorted_set", -1.7, "member") - 10.0 # Negetive increment, decrements the score. + 10.0 # Negative increment, decrements the score. >>> await client.zincrby("my_sorted_set", 5.5, "non_existing_member") - 5.5 # A new memeber is added to the sorted set with the score being 5.5. + 5.5 # A new member is added to the sorted set with the score being 5.5. """ return cast( float, From 596ebea1deaa811ebb50c109e5ec61924e9fca9b Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Thu, 25 Jul 2024 16:47:12 -0700 Subject: [PATCH 055/236] Node: Convert classes to types (#2005) * Converted LPosOptions class to type Signed-off-by: Guian Gumpac * Rebased and resolved conflicts Signed-off-by: Guian Gumpac * Run linter Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- CHANGELOG.md | 3 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 30 +- node/src/Commands.ts | 186 ++++++++++-- node/src/GlideClient.ts | 2 +- node/src/GlideClusterClient.ts | 2 +- node/src/Transaction.ts | 10 +- node/src/commands/BitOffsetOptions.ts | 48 --- node/src/commands/ConditionalChange.ts | 21 -- node/src/commands/FlushMode.ts | 23 -- node/src/commands/LPosOptions.ts | 64 ---- node/src/commands/geospatial/GeoAddOptions.ts | 52 ---- .../src/commands/geospatial/GeospatialData.ts | 37 --- node/tests/RedisClient.test.ts | 2 +- node/tests/RedisClusterClient.test.ts | 2 +- node/tests/SharedTests.ts | 281 ++++++++---------- node/tests/TestUtilities.ts | 43 +-- 17 files changed, 326 insertions(+), 482 deletions(-) delete mode 100644 node/src/commands/BitOffsetOptions.ts delete mode 100644 node/src/commands/ConditionalChange.ts delete mode 100644 node/src/commands/FlushMode.ts delete mode 100644 node/src/commands/LPosOptions.ts delete mode 100644 node/src/commands/geospatial/GeoAddOptions.ts delete mode 100644 node/src/commands/geospatial/GeospatialData.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 857f9725ca..aa2e4b5e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) +#### Breaking Changes +* Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) + #### Fixes * Java: Add overloads for XADD to allow duplicate entry keys ([#1970](https://github.com/valkey-io/valkey-glide/pull/1970)) * Node: Fix ZADD bug where command could not be called with only the `changed` optional parameter ([#1995](https://github.com/valkey-io/valkey-glide/pull/1995)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 420529e0e7..16585715f3 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -101,6 +101,7 @@ function initialize() { SetOptions, ZaddOptions, ScoreBoundry, + UpdateOptions, RangeByIndex, RangeByScore, RangeByLex, @@ -157,6 +158,7 @@ function initialize() { SetOptions, ZaddOptions, ScoreBoundry, + UpdateOptions, RangeByIndex, RangeByScore, RangeByLex, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ac62c35bcd..87a2df8531 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -13,11 +13,15 @@ import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, BitmapIndexType, + BitOffsetOptions, BitwiseOperation, ExpireOptions, + GeoAddOptions, + GeospatialData, GeoUnit, InsertPosition, KeyWeight, + LPosOptions, ListDirection, RangeByIndex, RangeByLex, @@ -140,10 +144,6 @@ import { createZRevRankWithScore, createZScore, } from "./Commands"; -import { BitOffsetOptions } from "./commands/BitOffsetOptions"; -import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; -import { GeospatialData } from "./commands/geospatial/GeospatialData"; -import { LPosOptions } from "./commands/LPosOptions"; import { ClosingError, ConfigurationError, @@ -2388,7 +2388,8 @@ export class BaseClient { * @example * ```typescript * // Example usage of the zadd method to update scores in an existing sorted set - * const result = await client.zadd("existing_sorted_set", { member1: 15.0, member2: 5.5 }, { conditionalChange: "onlyIfExists", changed: true }); + * const options = { conditionalChange: ConditionalChange.ONLY_IF_EXISTS, changed: true }; + * const result = await client.zadd("existing_sorted_set", { member1: 15.0, member2: 5.5 }, options); * console.log(result); // Output: 2 - Updates the scores of two existing members in the sorted set "existing_sorted_set." * ``` */ @@ -3567,8 +3568,8 @@ export class BaseClient { * @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". + * console.log(await client.lpos("myList", "e", { rank: 2 })); // Output: 5 - the second occurrence of "e" is at index 5. + * console.log(await client.lpos("myList", "e", { count: 3 })); // Output: [ 4, 5 ] - indices for the occurrences of "e" in list "myList". * ``` */ public lpos( @@ -3594,9 +3595,9 @@ export class BaseClient { * @example * ```typescript * console.log(await client.bitcount("my_key1")); // Output: 2 - The string stored at "my_key1" contains 2 set bits. - * console.log(await client.bitcount("my_key2", OffsetOptions(1, 3))); // Output: 2 - The second to fourth bytes of the string stored at "my_key2" contain 2 set bits. - * console.log(await client.bitcount("my_key3", OffsetOptions(1, 1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the second bit of the string stored at "my_key3" is set. - * console.log(await client.bitcount("my_key3", OffsetOptions(-1, -1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the last bit of the string stored at "my_key3" is set. + * console.log(await client.bitcount("my_key2", { start: 1, end: 3 })); // Output: 2 - The second to fourth bytes of the string stored at "my_key2" contain 2 set bits. + * console.log(await client.bitcount("my_key3", { start: 1, end: 1, indexType: BitmapIndexType.BIT })); // Output: 1 - Indicates that the second bit of the string stored at "my_key3" is set. + * console.log(await client.bitcount("my_key3", { start: -1, end: -1, indexType: BitmapIndexType.BIT })); // Output: 1 - Indicates that the last bit of the string stored at "my_key3" is set. * ``` */ public bitcount(key: string, options?: BitOffsetOptions): Promise { @@ -3619,8 +3620,11 @@ export class BaseClient { * * @example * ```typescript - * const options = new GeoAddOptions({updateMode: ConditionalChange.ONLY_IF_EXISTS, changed: true}); - * const num = await client.geoadd("mySortedSet", new Map([["Palermo", new GeospatialData(13.361389, 38.115556)]]), options); + * const options = {updateMode: ConditionalChange.ONLY_IF_EXISTS, changed: true}; + * const membersToCoordinates = new Map([ + * ["Palermo", { longitude: 13.361389, latitude: 38.115556 }], + * ]); + * const num = await client.geoadd("mySortedSet", membersToCoordinates, options); * console.log(num); // Output: 1 - Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated. * ``` */ @@ -3648,7 +3652,7 @@ export class BaseClient { * * @example * ```typescript - * const data = new Map([["Palermo", new GeospatialData(13.361389, 38.115556)], ["Catania", new GeospatialData(15.087269, 37.502669)]]); + * const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]); * await client.geoadd("mySortedSet", data); * const result = await client.geopos("mySortedSet", ["Palermo", "Catania", "NonExisting"]); * // When added via GEOADD, the geospatial coordinates are converted into a 52 bit geohash, so the coordinates diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 044ddc9759..ff4eac85ae 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -4,13 +4,14 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; -import { FlushMode } from "./commands/FlushMode"; -import { LPosOptions } from "./commands/LPosOptions"; +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { BaseClient } from "src/BaseClient"; +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { GlideClient } from "src/GlideClient"; +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { GlideClusterClient } from "src/GlideClusterClient"; import { command_request } from "./ProtobufMessage"; -import { BitOffsetOptions } from "./commands/BitOffsetOptions"; -import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; -import { GeospatialData } from "./commands/geospatial/GeospatialData"; import RequestType = command_request.RequestType; @@ -976,21 +977,25 @@ export function createTTL(key: string): command_request.Command { return createCommand(RequestType.TTL, [key]); } +/** + * Options for updating elements of a sorted set key. + */ +export enum UpdateByScore { + /** Only update existing elements if the new score is less than the current score. */ + LESS_THAN = "LT", + /** Only update existing elements if the new score is greater than the current score. */ + GREATER_THAN = "GT", +} + export type ZAddOptions = { /** - * `onlyIfDoesNotExist` - Only add new elements. Don't update already existing - * elements. Equivalent to `NX` in the Redis API. `onlyIfExists` - Only update - * elements that already exist. Don't add new elements. Equivalent to `XX` in - * the Redis API. + * Options for handling existing members. */ - conditionalChange?: "onlyIfExists" | "onlyIfDoesNotExist"; + conditionalChange?: ConditionalChange; /** - * `scoreLessThanCurrent` - Only update existing elements if the new score is - * less than the current score. Equivalent to `LT` in the Redis API. - * `scoreGreaterThanCurrent` - Only update existing elements if the new score - * is greater than the current score. Equivalent to `GT` in the Redis API. + * Options for updating scores. */ - updateOptions?: "scoreLessThanCurrent" | "scoreGreaterThanCurrent"; + updateOptions?: UpdateByScore; /** * Modify the return value from the number of new elements added, to the total number of elements changed. */ @@ -1009,22 +1014,22 @@ export function createZAdd( let args = [key]; if (options) { - if (options.conditionalChange === "onlyIfExists") { - args.push("XX"); - } else if (options.conditionalChange === "onlyIfDoesNotExist") { - if (options.updateOptions) { + if (options.conditionalChange) { + if ( + options.conditionalChange === + ConditionalChange.ONLY_IF_DOES_NOT_EXIST && + options.updateOptions + ) { throw new Error( `The GT, LT, and NX options are mutually exclusive. Cannot choose both ${options.updateOptions} and NX.`, ); } - args.push("NX"); + args.push(options.conditionalChange); } - if (options.updateOptions === "scoreLessThanCurrent") { - args.push("LT"); - } else if (options.updateOptions === "scoreGreaterThanCurrent") { - args.push("GT"); + if (options.updateOptions) { + args.push(options.updateOptions); } if (options.changed) { @@ -1694,6 +1699,27 @@ export function createFunctionLoad( return createCommand(RequestType.FunctionLoad, args); } +/** + * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are + * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. + * The offsets can also be negative numbers indicating offsets starting at the end of the string, with `-1` being + * the last index of the string, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitcount/ for more details. + */ +export type BitOffsetOptions = { + /** The starting offset index. */ + start: number; + /** The ending offset index. */ + end: number; + /** + * The index offset type. This option can only be specified if you are using server version 7.0.0 or above. + * Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. + * If no index type is provided, the indexes will be assumed to be byte indexes. + */ + indexType?: BitmapIndexType; +}; + /** * @internal */ @@ -1702,7 +1728,13 @@ export function createBitCount( options?: BitOffsetOptions, ): command_request.Command { const args = [key]; - if (options) args.push(...options.toArgs()); + + if (options) { + args.push(options.start.toString()); + args.push(options.end.toString()); + if (options.indexType) args.push(options.indexType); + } + return createCommand(RequestType.BitCount, args); } @@ -1747,6 +1779,24 @@ export function createBitPos( return createCommand(RequestType.BitPos, args); } +/** + * Defines flushing mode for {@link GlideClient.flushall}, {@link GlideClusterClient.flushall}, + * {@link GlideClient.functionFlush}, {@link GlideClusterClient.functionFlush}, + * {@link GlideClient.flushdb} and {@link GlideClusterClient.flushdb} commands. + * + * See https://valkey.io/commands/flushall/ and https://valkey.io/commands/flushdb/ for details. + */ +export enum FlushMode { + /** + * Flushes synchronously. + * + * since Valkey version 6.2.0. + */ + SYNC = "SYNC", + /** Flushes asynchronously. */ + ASYNC = "ASYNC", +} + export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or @@ -1932,6 +1982,20 @@ export function createFlushDB(mode?: FlushMode): command_request.Command { } } +/** + * Optional arguments to LPOS command. + * + * See https://valkey.io/commands/lpos/ for more details. + */ +export type LPosOptions = { + /** The rank of the match to return. */ + rank?: number; + /** The specific number of matching indices from a list. */ + count?: number; + /** The maximum number of comparisons to make between the element and the items in the list. */ + maxLength?: number; +}; + /** * @internal */ @@ -1940,10 +2004,23 @@ export function createLPos( element: string, options?: LPosOptions, ): command_request.Command { - let args: string[] = [key, element]; + const args: string[] = [key, element]; if (options) { - args = args.concat(options.toArgs()); + if (options.rank !== undefined) { + args.push("RANK"); + args.push(options.rank.toString()); + } + + if (options.count !== undefined) { + args.push("COUNT"); + args.push(options.count.toString()); + } + + if (options.maxLength !== undefined) { + args.push("MAXLEN"); + args.push(options.maxLength.toString()); + } } return createCommand(RequestType.LPos, args); @@ -1956,6 +2033,48 @@ export function createDBSize(): command_request.Command { return createCommand(RequestType.DBSize, []); } +/** + * An optional condition to the {@link BaseClient.geoadd} command. + */ +export enum ConditionalChange { + /** + * Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Valkey API. + */ + ONLY_IF_EXISTS = "XX", + + /** + * Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Valkey API. + */ + ONLY_IF_DOES_NOT_EXIST = "NX", +} + +/** + * Represents a geographic position defined by longitude and latitude. + * The exact limits, as specified by `EPSG:900913 / EPSG:3785 / OSGEO:41001` are the + * following: + * + * Valid longitudes are from `-180` to `180` degrees. + * Valid latitudes are from `-85.05112878` to `85.05112878` degrees. + */ +export type GeospatialData = { + /** The longitude coordinate. */ + longitude: number; + /** The latitude coordinate. */ + latitude: number; +}; + +/** + * Optional arguments for the GeoAdd command. + * + * See https://valkey.io/commands/geoadd/ for more details. + */ +export type GeoAddOptions = { + /** Options for handling existing members. See {@link ConditionalChange}. */ + updateMode?: ConditionalChange; + /** If `true`, returns the count of changed elements instead of new elements added. */ + changed?: boolean; +}; + /** * @internal */ @@ -1967,11 +2086,20 @@ export function createGeoAdd( let args: string[] = [key]; if (options) { - args = args.concat(options.toArgs()); + if (options.updateMode) { + args.push(options.updateMode); + } + + if (options.changed) { + args.push("CH"); + } } membersToGeospatialData.forEach((coord, member) => { - args = args.concat(coord.toArgs()); + args = args.concat([ + coord.longitude.toString(), + coord.latitude.toString(), + ]); args.push(member); }); return createCommand(RequestType.GeoAdd, args); diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index b89e39a6c4..c63715b6df 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -10,6 +10,7 @@ import { ReturnType, } from "./BaseClient"; import { + FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -35,7 +36,6 @@ import { } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; -import { FlushMode } from "./commands/FlushMode"; /* eslint-disable-next-line @typescript-eslint/no-namespace */ export namespace GlideClientConfiguration { diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 0cddacdb0f..632132a3e0 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -10,6 +10,7 @@ import { ReturnType, } from "./BaseClient"; import { + FlushMode, InfoOptions, LolwutOptions, createClientGetName, @@ -34,7 +35,6 @@ import { createPublish, createTime, } from "./Commands"; -import { FlushMode } from "./commands/FlushMode"; import { RequestError } from "./Errors"; import { command_request, connection_request } from "./ProtobufMessage"; import { ClusterTransaction } from "./Transaction"; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 9c4aaeb347..df3a75afd1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,13 +4,18 @@ import { AggregationType, + BitOffsetOptions, BitmapIndexType, BitwiseOperation, ExpireOptions, + FlushMode, + GeoAddOptions, + GeospatialData, GeoUnit, InfoOptions, InsertPosition, KeyWeight, + LPosOptions, ListDirection, LolwutOptions, RangeByIndex, @@ -154,11 +159,6 @@ import { createZIncrBy, } from "./Commands"; import { command_request } from "./ProtobufMessage"; -import { BitOffsetOptions } from "./commands/BitOffsetOptions"; -import { FlushMode } from "./commands/FlushMode"; -import { LPosOptions } from "./commands/LPosOptions"; -import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; -import { GeospatialData } from "./commands/geospatial/GeospatialData"; /** * Base class encompassing shared commands for both standalone and cluster mode implementations in a transaction. diff --git a/node/src/commands/BitOffsetOptions.ts b/node/src/commands/BitOffsetOptions.ts deleted file mode 100644 index 5f5d6800e6..0000000000 --- a/node/src/commands/BitOffsetOptions.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -// Import below added to fix up the TSdoc link, but eslint blames for unused import. -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { BaseClient } from "src/BaseClient"; -import { BitmapIndexType } from "src/Commands"; - -/** - * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are - * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. - * The offsets can also be negative numbers indicating offsets starting at the end of the string, with `-1` being - * the last index of the string, `-2` being the penultimate, and so on. - * - * See https://valkey.io/commands/bitcount/ for more details. - */ -export class BitOffsetOptions { - private start: number; - private end: number; - private indexType?: BitmapIndexType; - - /** - * @param start - The starting offset index. - * @param end - The ending offset index. - * @param indexType - The index offset type. This option can only be specified if you are using server version 7.0.0 or above. - * Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. - * If no index type is provided, the indexes will be assumed to be byte indexes. - */ - constructor(start: number, end: number, indexType?: BitmapIndexType) { - this.start = start; - this.end = end; - this.indexType = indexType; - } - - /** - * Converts BitOffsetOptions into a string[]. - * - * @returns string[] - */ - public toArgs(): string[] { - const args = [this.start.toString(), this.end.toString()]; - - if (this.indexType) args.push(this.indexType); - - return args; - } -} diff --git a/node/src/commands/ConditionalChange.ts b/node/src/commands/ConditionalChange.ts deleted file mode 100644 index 5904f90d32..0000000000 --- a/node/src/commands/ConditionalChange.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { BaseClient } from "src/BaseClient"; - -/** - * An optional condition to the {@link BaseClient.geoadd} command. - */ -export enum ConditionalChange { - /** - * Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Valkey API. - */ - ONLY_IF_EXISTS = "XX", - - /** - * Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Valkey API. - * */ - ONLY_IF_DOES_NOT_EXIST = "NX", -} diff --git a/node/src/commands/FlushMode.ts b/node/src/commands/FlushMode.ts deleted file mode 100644 index 78b9ca10c0..0000000000 --- a/node/src/commands/FlushMode.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -// Import below added to fix up the TSdoc link, but eslint blames for unused import. -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { GlideClient } from "src/GlideClient"; - -/** - * Defines flushing mode for {@link GlideClient.flushall|flushall} and {@link GlideClient.flushdb|flushdb} commands. - * - * See https://valkey.io/commands/flushall/ and https://valkey.io/commands/flushdb/ for details. - */ -export enum FlushMode { - /** - * Flushes synchronously. - * - * since Valkey version 6.2.0. - */ - SYNC = "SYNC", - /** Flushes asynchronously. */ - ASYNC = "ASYNC", -} diff --git a/node/src/commands/LPosOptions.ts b/node/src/commands/LPosOptions.ts deleted file mode 100644 index de2c0bcc2a..0000000000 --- a/node/src/commands/LPosOptions.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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/src/commands/geospatial/GeoAddOptions.ts b/node/src/commands/geospatial/GeoAddOptions.ts deleted file mode 100644 index 220dff8e19..0000000000 --- a/node/src/commands/geospatial/GeoAddOptions.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -import { ConditionalChange } from "../ConditionalChange"; - -/** - * Optional arguments for the GeoAdd command. - * - * See https://valkey.io/commands/geoadd/ for more details. - */ -export class GeoAddOptions { - /** Valkey API keyword use to modify the return value from the number of new elements added, to the total number of elements changed. */ - public static CHANGED_VALKEY_API = "CH"; - - private updateMode?: ConditionalChange; - - private changed?: boolean; - - /** - * Default constructor for GeoAddOptions. - * - * @param updateMode - Options for handling existing members. See {@link ConditionalChange}. - * @param latitude - If `true`, returns the count of changed elements instead of new elements added. - */ - constructor(options: { - updateMode?: ConditionalChange; - changed?: boolean; - }) { - this.updateMode = options.updateMode; - this.changed = options.changed; - } - - /** - * Converts GeoAddOptions into a string[]. - * - * @returns string[] - */ - public toArgs(): string[] { - const args: string[] = []; - - if (this.updateMode) { - args.push(this.updateMode); - } - - if (this.changed) { - args.push(GeoAddOptions.CHANGED_VALKEY_API); - } - - return args; - } -} diff --git a/node/src/commands/geospatial/GeospatialData.ts b/node/src/commands/geospatial/GeospatialData.ts deleted file mode 100644 index 63c15bdff0..0000000000 --- a/node/src/commands/geospatial/GeospatialData.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -/** - * Represents a geographic position defined by longitude and latitude. - * The exact limits, as specified by `EPSG:900913 / EPSG:3785 / OSGEO:41001` are the - * following: - * - * Valid longitudes are from `-180` to `180` degrees. - * Valid latitudes are from `-85.05112878` to `85.05112878` degrees. - */ -export class GeospatialData { - private longitude: number; - - private latitude: number; - - /** - * Default constructor for GeospatialData. - * - * @param longitude - The longitude coordinate. - * @param latitude - The latitude coordinate. - */ - constructor(longitude: number, latitude: number) { - this.longitude = longitude; - this.latitude = latitude; - } - - /** - * Converts GeospatialData into a string[]. - * - * @returns string[] - */ - public toArgs(): string[] { - return [this.longitude.toString(), this.latitude.toString()]; - } -} diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 09ac745aeb..e8326ab797 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -14,7 +14,7 @@ import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { GlideClient, ProtocolVersion, Transaction } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/commands/FlushMode.js"; +import { FlushMode } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 75a68dcdd6..79713181a2 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -21,8 +21,8 @@ import { Routes, ScoreFilter, } from ".."; +import { FlushMode } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index e0bd789c40..12ba6d97cd 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -13,7 +13,11 @@ import { BitmapIndexType, BitwiseOperation, ClosingError, + ConditionalChange, ExpireOptions, + FlushMode, + GeoUnit, + GeospatialData, ListDirection, GlideClient, GlideClusterClient, @@ -23,9 +27,11 @@ import { RequestError, ScoreFilter, Script, + UpdateByScore, parseInfoResponse, - GeoUnit, } from "../"; +import { RedisCluster } from "../../utils/TestUtils"; +import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { Client, GetAndSetRandomValue, @@ -35,14 +41,6 @@ import { intoArray, intoString, } from "./TestUtilities"; -import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; -import { BitOffsetOptions } from "../build-ts/src/commands/BitOffsetOptions"; -import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; -import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; -import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions"; -import { ConditionalChange } from "../build-ts/src/commands/ConditionalChange"; -import { FlushMode } from "../build-ts/src/commands/FlushMode"; -import { RedisCluster } from "../../utils/TestUtils"; export type BaseClient = GlideClient | GlideClusterClient; @@ -283,8 +281,6 @@ export function runBaseTests(config: { await client.set("foo", "bar"); const oldResult = await client.info([InfoOptions.Commandstats]); const oldResultAsString = intoString(oldResult); - console.log(oldResult); - console.log(oldResultAsString); expect(oldResultAsString).toContain("cmdstat_set"); checkSimple(await client.configResetStat()).toEqual("OK"); @@ -2450,25 +2446,27 @@ export function runBaseTests(config: { const membersScores = { one: 1, two: 2, three: 3 }; expect( await client.zadd(key, membersScores, { - conditionalChange: "onlyIfExists", + conditionalChange: ConditionalChange.ONLY_IF_EXISTS, }), ).toEqual(0); expect( await client.zadd(key, membersScores, { - conditionalChange: "onlyIfDoesNotExist", + conditionalChange: + ConditionalChange.ONLY_IF_DOES_NOT_EXIST, }), ).toEqual(3); expect( await client.zaddIncr(key, "one", 5.0, { - conditionalChange: "onlyIfDoesNotExist", + conditionalChange: + ConditionalChange.ONLY_IF_DOES_NOT_EXIST, }), ).toEqual(null); expect( await client.zaddIncr(key, "one", 5.0, { - conditionalChange: "onlyIfExists", + conditionalChange: ConditionalChange.ONLY_IF_EXISTS, }), ).toEqual(6.0); }, protocol); @@ -2488,27 +2486,27 @@ export function runBaseTests(config: { expect( await client.zadd(key, membersScores, { - updateOptions: "scoreGreaterThanCurrent", + updateOptions: UpdateByScore.GREATER_THAN, changed: true, }), ).toEqual(1); expect( await client.zadd(key, membersScores, { - updateOptions: "scoreLessThanCurrent", + updateOptions: UpdateByScore.LESS_THAN, changed: true, }), ).toEqual(0); expect( await client.zaddIncr(key, "one", -3.0, { - updateOptions: "scoreLessThanCurrent", + updateOptions: UpdateByScore.LESS_THAN, }), ).toEqual(7.0); expect( await client.zaddIncr(key, "one", -3.0, { - updateOptions: "scoreGreaterThanCurrent", + updateOptions: UpdateByScore.GREATER_THAN, }), ).toEqual(null); }, protocol); @@ -4540,44 +4538,32 @@ export function runBaseTests(config: { // simplest case expect(await client.lpos(key, "a")).toEqual(0); - expect( - await client.lpos(key, "b", new LPosOptions({ rank: 2 })), - ).toEqual(5); + expect(await client.lpos(key, "b", { 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); + expect(await client.lpos(key, "b", { rank: -2 })).toEqual(2); // unlimited comparisons expect( - await client.lpos( - key, - "a", - new LPosOptions({ rank: 1, maxLength: 0 }), - ), + await client.lpos(key, "a", { rank: 1, maxLength: 0 }), ).toEqual(0); // limited comparisons expect( - await client.lpos( - key, - "c", - new LPosOptions({ rank: 1, maxLength: 2 }), - ), + await client.lpos(key, "c", { rank: 1, maxLength: 2 }), ).toBeNull(); // invalid rank value await expect( - client.lpos(key, "a", new LPosOptions({ rank: 0 })), + client.lpos(key, "a", { rank: 0 }), ).rejects.toThrow(RequestError); // invalid maxlen value await expect( - client.lpos(key, "a", new LPosOptions({ maxLength: -1 })), + client.lpos(key, "a", { maxLength: -1 }), ).rejects.toThrow(RequestError); // non-existent key @@ -4593,45 +4579,29 @@ export function runBaseTests(config: { // invalid count value await expect( - client.lpos(key, "a", new LPosOptions({ count: -1 })), + client.lpos(key, "a", { count: -1 }), ).rejects.toThrow(RequestError); // with count + expect(await client.lpos(key, "a", { count: 2 })).toEqual([ + 0, 1, + ]); + expect(await client.lpos(key, "a", { count: 0 })).toEqual([ + 0, 1, 4, + ]); expect( - await client.lpos(key, "a", new LPosOptions({ count: 2 })), - ).toEqual([0, 1]); - expect( - await client.lpos(key, "a", new LPosOptions({ count: 0 })), + await client.lpos(key, "a", { rank: 1, 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 }), - ), + await client.lpos(key, "a", { rank: 2, count: 0 }), ).toEqual([1, 4]); expect( - await client.lpos( - key, - "a", - new LPosOptions({ rank: 3, count: 0 }), - ), + await client.lpos(key, "a", { rank: 3, count: 0 }), ).toEqual([4]); // reverse traversal expect( - await client.lpos( - key, - "a", - new LPosOptions({ rank: -1, count: 0 }), - ), + await client.lpos(key, "a", { rank: -1, count: 0 }), ).toEqual([4, 1, 0]); }, protocol); }, @@ -4697,18 +4667,15 @@ export function runBaseTests(config: { checkSimple(await client.set(key1, value)).toEqual("OK"); expect(await client.bitcount(key1)).toEqual(26); expect( - await client.bitcount(key1, new BitOffsetOptions(1, 1)), + await client.bitcount(key1, { start: 1, end: 1 }), ).toEqual(6); expect( - await client.bitcount(key1, new BitOffsetOptions(0, -5)), + await client.bitcount(key1, { start: 0, end: -5 }), ).toEqual(10); // non-existing key expect(await client.bitcount(uuidv4())).toEqual(0); expect( - await client.bitcount( - uuidv4(), - new BitOffsetOptions(5, 30), - ), + await client.bitcount(uuidv4(), { start: 5, end: 30 }), ).toEqual(0); // key exists, but it is not a string expect(await client.sadd(key2, [value])).toEqual(1); @@ -4716,53 +4683,60 @@ export function runBaseTests(config: { RequestError, ); await expect( - client.bitcount(key2, new BitOffsetOptions(1, 1)), + client.bitcount(key2, { start: 1, end: 1 }), ).rejects.toThrow(RequestError); if (cluster.checkIfServerVersionLessThan("7.0.0")) { await expect( - client.bitcount( - key1, - new BitOffsetOptions(2, 5, BitmapIndexType.BIT), - ), + client.bitcount(key1, { + start: 2, + end: 5, + indexType: BitmapIndexType.BIT, + }), ).rejects.toThrow(); await expect( - client.bitcount( - key1, - new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), - ), + client.bitcount(key1, { + start: 2, + end: 5, + indexType: BitmapIndexType.BYTE, + }), ).rejects.toThrow(); } else { expect( - await client.bitcount( - key1, - new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), - ), + await client.bitcount(key1, { + start: 2, + end: 5, + indexType: BitmapIndexType.BYTE, + }), ).toEqual(16); expect( - await client.bitcount( - key1, - new BitOffsetOptions(5, 30, BitmapIndexType.BIT), - ), + await client.bitcount(key1, { + start: 5, + end: 30, + indexType: BitmapIndexType.BIT, + }), ).toEqual(17); expect( - await client.bitcount( - key1, - new BitOffsetOptions(5, -5, BitmapIndexType.BIT), - ), + await client.bitcount(key1, { + start: 5, + end: -5, + indexType: BitmapIndexType.BIT, + }), ).toEqual(23); expect( - await client.bitcount( - uuidv4(), - new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), - ), + await client.bitcount(uuidv4(), { + start: 2, + end: 5, + indexType: BitmapIndexType.BYTE, + }), ).toEqual(0); // key exists, but it is not a string await expect( - client.bitcount( - key2, - new BitOffsetOptions(1, 1, BitmapIndexType.BYTE), - ), + client.bitcount(key2, { + start: 1, + end: 1, + indexType: BitmapIndexType.BYTE, + }), ).rejects.toThrow(RequestError); } }, protocol); @@ -4777,14 +4751,14 @@ export function runBaseTests(config: { const key1 = uuidv4(); const key2 = uuidv4(); const membersToCoordinates = new Map(); - membersToCoordinates.set( - "Palermo", - new GeospatialData(13.361389, 38.115556), - ); - membersToCoordinates.set( - "Catania", - new GeospatialData(15.087269, 37.502669), - ); + membersToCoordinates.set("Palermo", { + longitude: 13.361389, + latitude: 38.115556, + }); + membersToCoordinates.set("Catania", { + longitude: 15.087269, + latitude: 37.502669, + }); // default geoadd expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); @@ -4812,45 +4786,34 @@ export function runBaseTests(config: { expect(geopos).toEqual([null]); // with update mode options - membersToCoordinates.set( - "Catania", - new GeospatialData(15.087269, 39), - ); + membersToCoordinates.set("Catania", { + longitude: 15.087269, + latitude: 39, + }); expect( - await client.geoadd( - key1, - membersToCoordinates, - new GeoAddOptions({ - updateMode: - ConditionalChange.ONLY_IF_DOES_NOT_EXIST, - }), - ), + await client.geoadd(key1, membersToCoordinates, { + updateMode: ConditionalChange.ONLY_IF_DOES_NOT_EXIST, + }), ).toBe(0); expect( - await client.geoadd( - key1, - membersToCoordinates, - new GeoAddOptions({ - updateMode: ConditionalChange.ONLY_IF_EXISTS, - }), - ), + await client.geoadd(key1, membersToCoordinates, { + updateMode: ConditionalChange.ONLY_IF_EXISTS, + }), ).toBe(0); // with changed option - membersToCoordinates.set( - "Catania", - new GeospatialData(15.087269, 40), - ); - membersToCoordinates.set( - "Tel-Aviv", - new GeospatialData(32.0853, 34.7818), - ); + membersToCoordinates.set("Catania", { + longitude: 15.087269, + latitude: 40, + }); + membersToCoordinates.set("Tel-Aviv", { + longitude: 32.0853, + latitude: 34.7818, + }); expect( - await client.geoadd( - key1, - membersToCoordinates, - new GeoAddOptions({ changed: true }), - ), + await client.geoadd(key1, membersToCoordinates, { + changed: true, + }), ).toBe(2); // key exists but holding non-zset value @@ -4877,25 +4840,25 @@ export function runBaseTests(config: { await expect( client.geoadd( key, - new Map([["Place", new GeospatialData(-181, 0)]]), + new Map([["Place", { longitude: -181, latitude: 0 }]]), ), ).rejects.toThrow(); await expect( client.geoadd( key, - new Map([["Place", new GeospatialData(181, 0)]]), + new Map([["Place", { longitude: 181, latitude: 0 }]]), ), ).rejects.toThrow(); await expect( client.geoadd( key, - new Map([["Place", new GeospatialData(0, 86)]]), + new Map([["Place", { longitude: 0, latitude: 86 }]]), ), ).rejects.toThrow(); await expect( client.geoadd( key, - new Map([["Place", new GeospatialData(0, -86)]]), + new Map([["Place", { longitude: 0, latitude: -86 }]]), ), ).rejects.toThrow(); }, protocol); @@ -5016,14 +4979,14 @@ export function runBaseTests(config: { // adding the geo locations const membersToCoordinates = new Map(); - membersToCoordinates.set( - member1, - new GeospatialData(13.361389, 38.115556), - ); - membersToCoordinates.set( - member2, - new GeospatialData(15.087269, 37.502669), - ); + membersToCoordinates.set(member1, { + longitude: 13.361389, + latitude: 38.115556, + }); + membersToCoordinates.set(member2, { + longitude: 15.087269, + latitude: 37.502669, + }); expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); // checking result with default metric @@ -5068,14 +5031,14 @@ export function runBaseTests(config: { // adding the geo locations const membersToCoordinates = new Map(); - membersToCoordinates.set( - "Palermo", - new GeospatialData(13.361389, 38.115556), - ); - membersToCoordinates.set( - "Catania", - new GeospatialData(15.087269, 37.502669), - ); + membersToCoordinates.set("Palermo", { + longitude: 13.361389, + latitude: 38.115556, + }); + membersToCoordinates.set("Catania", { + longitude: 15.087269, + latitude: 37.502669, + }); expect(await client.geoadd(key1, membersToCoordinates)).toBe(2); // checking result with default metric diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fbe9722921..9f23326bc6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -13,7 +13,9 @@ import { BitmapIndexType, BitwiseOperation, ClusterTransaction, + FlushMode, GeoUnit, + GeospatialData, GlideClient, GlideClusterClient, InsertPosition, @@ -24,10 +26,6 @@ import { ScoreFilter, Transaction, } from ".."; -import { BitOffsetOptions } from "../build-ts/src/commands/BitOffsetOptions"; -import { FlushMode } from "../build-ts/src/commands/FlushMode"; -import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; -import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; beforeAll(() => { Logger.init("info"); @@ -526,20 +524,10 @@ export async function transactionTest( field + "3", ]); responseData.push(["rpush(key16, [1, 1, 2, 3, 3,])", 5]); - baseTransaction.lpos(key16, field + "1", new LPosOptions({ rank: 2 })); - responseData.push([ - 'lpos(key16, field + "1", new LPosOptions({ rank: 2 }))', - 1, - ]); - baseTransaction.lpos( - key16, - field + "1", - new LPosOptions({ rank: 2, count: 0 }), - ); - responseData.push([ - 'lpos(key16, field + "1", new LPosOptions({ rank: 2, count: 0 }))', - [1], - ]); + baseTransaction.lpos(key16, field + "1", { rank: 2 }); + responseData.push(['lpos(key16, field + "1", { rank: 2 })', 1]); + baseTransaction.lpos(key16, field + "1", { rank: 2, count: 0 }); + responseData.push(['lpos(key16, field + "1", { rank: 2, count: 0 })', [1]]); baseTransaction.sadd(key7, ["bar", "foo"]); responseData.push(['sadd(key7, ["bar", "foo"])', 2]); baseTransaction.sunionstore(key7, [key7, key7]); @@ -743,8 +731,8 @@ export async function transactionTest( responseData.push(['set(key17, "foobar")', "OK"]); baseTransaction.bitcount(key17); responseData.push(["bitcount(key17)", 26]); - baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); - responseData.push(["bitcount(key17, new BitOffsetOptions(1, 1))", 6]); + baseTransaction.bitcount(key17, { start: 1, end: 1 }); + responseData.push(["bitcount(key17, { start: 1, end: 1 })", 6]); baseTransaction.bitpos(key17, 1); responseData.push(["bitpos(key17, 1)", 1]); @@ -759,10 +747,11 @@ export async function transactionTest( responseData.push(["get(key19)", "`bc`ab"]); if (gte("7.0.0", version)) { - baseTransaction.bitcount( - key17, - new BitOffsetOptions(5, 30, BitmapIndexType.BIT), - ); + baseTransaction.bitcount(key17, { + start: 5, + end: 30, + indexType: BitmapIndexType.BIT, + }); responseData.push([ "bitcount(key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT))", 17, @@ -780,9 +769,9 @@ export async function transactionTest( responseData.push(["pfcount([key11])", 3]); baseTransaction.geoadd( key18, - new Map([ - ["Palermo", new GeospatialData(13.361389, 38.115556)], - ["Catania", new GeospatialData(15.087269, 37.502669)], + new Map([ + ["Palermo", { longitude: 13.361389, latitude: 38.115556 }], + ["Catania", { longitude: 15.087269, latitude: 37.502669 }], ]), ); responseData.push(["geoadd(key18, { Palermo: ..., Catania: ... })", 2]); From 33016aac114e10c8c2fbbad3cff6519f92680daf Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Thu, 25 Jul 2024 17:25:13 -0700 Subject: [PATCH 056/236] Node: add `BZMPOP` command (#2018) * Added BZMPOP command. Signed-off-by: Guian Gumpac Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 51 ++++++++++++++-- node/src/Commands.ts | 24 ++++++++ node/src/Transaction.ts | 34 ++++++++++- node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 86 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 12 ++++ 7 files changed, 204 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2e4b5e71..10e2e3cf99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) +* Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 87a2df8531..ef25d8be0e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -37,6 +37,7 @@ import { createBRPop, createBitCount, createBitOp, + createBZMPop, createBitPos, createDecr, createDecrBy, @@ -3677,7 +3678,7 @@ export class BaseClient { * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. - * @param count - The number of elements to pop. + * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. * @returns A two-element `array` containing the key name of the set from which the element * was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. @@ -3692,12 +3693,54 @@ export class BaseClient { * // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1". * ``` */ - public zmpop( - key: string[], + public async zmpop( + keys: string[], + modifier: ScoreFilter, + count?: number, + ): Promise<[string, [Record]] | null> { + return this.createWritePromise(createZMPop(keys, modifier, count)); + } + + /** + * Pops a member-score pair from the first non-empty sorted set, with the given `keys` being + * checked in the order they are provided. Blocks the connection when there are no members + * to pop from any of the given sorted sets. `BZMPOP` is the blocking variant of {@link zmpop}. + * + * See https://valkey.io/commands/bzmpop/ for more details. + * + * @remarks + * 1. When in cluster mode, all `keys` must map to the same hash slot. + * 2. `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | the wiki} + * for more details and best practices. + * @param keys - The keys of the sorted sets. + * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or + * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. + * @param timeout - The number of seconds to wait for a blocking operation to complete. + * A value of 0 will block indefinitely. + * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. + * @returns A two-element `array` containing the key name of the set from which the element + * was popped, and a member-score `Record` of the popped element. + * If no member could be popped, returns `null`. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); + * await client.zadd("zSet2", { four: 4.0 }); + * console.log(await client.bzmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 0.1, 2)); + * // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1". + * ``` + */ + public async bzmpop( + keys: string[], modifier: ScoreFilter, + timeout: number, count?: number, ): Promise<[string, [Record]] | null> { - return this.createWritePromise(createZMPop(key, modifier, count)); + return this.createWritePromise( + createBZMPop(keys, modifier, timeout, count), + ); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ff4eac85ae..4c1c6254c3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2211,6 +2211,30 @@ export function createZMPop( return createCommand(RequestType.ZMPop, args); } +/** + * @internal + */ +export function createBZMPop( + keys: string[], + modifier: ScoreFilter, + timeout: number, + count?: number, +): command_request.Command { + const args: string[] = [ + timeout.toString(), + keys.length.toString(), + ...keys, + modifier, + ]; + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.BZMPop, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index df3a75afd1..b945655cbb 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -30,6 +30,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBZMPop, createBitCount, createBitOp, createBitPos, @@ -2170,7 +2171,7 @@ export class BaseTransaction> { * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. - * @param count - The number of elements to pop. + * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. * * Command Response - A two-element `array` containing the key name of the set from which the * element was popped, and a member-score `Record` of the popped element. @@ -2182,6 +2183,37 @@ export class BaseTransaction> { return this.addAndReturn(createZMPop(keys, modifier, count)); } + /** + * Pops a member-score pair from the first non-empty sorted set, with the given `keys` being + * checked in the order they are provided. Blocks the connection when there are no members + * to pop from any of the given sorted sets. `BZMPOP` is the blocking variant of {@link zmpop}. + * + * See https://valkey.io/commands/bzmpop/ for more details. + * + * @remarks `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | the wiki} + * for more details and best practices. + * @param keys - The keys of the sorted sets. + * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or + * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. + * @param timeout - The number of seconds to wait for a blocking operation to complete. + * A value of 0 will block indefinitely. + * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. + * + * Command Response - A two-element `array` containing the key name of the set from which the element + * was popped, and a member-score `Record` of the popped element. + * If no member could be popped, returns `null`. + * + * since Valkey version 7.0.0. + */ + public bzmpop( + keys: string[], + modifier: ScoreFilter, + timeout: number, + count?: number, + ): T { + return this.addAndReturn(createBZMPop(keys, modifier, timeout, count)); + } + /** * Increments the score of `member` in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score. diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 79713181a2..b60f064b69 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -333,6 +333,7 @@ describe("GlideClusterClient", () => { client.sintercard(["abc", "zxy", "lkn"]), client.zintercard(["abc", "zxy", "lkn"]), client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), + client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 12ba6d97cd..81fb10e214 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4964,6 +4964,92 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bzmpop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const nonExistingKey = "{key}-0" + uuidv4(); + const stringKey = "{key}-string" + uuidv4(); + + expect(await client.zadd(key1, { a1: 1, b1: 2 })).toEqual(2); + expect(await client.zadd(key2, { a2: 0.1, b2: 0.2 })).toEqual( + 2, + ); + + checkSimple( + await client.bzmpop([key1, key2], ScoreFilter.MAX, 0.1), + ).toEqual([key1, { b1: 2 }]); + checkSimple( + await client.bzmpop([key2, key1], ScoreFilter.MAX, 0.1, 10), + ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); + + // ensure that command doesn't time out even if timeout > request timeout (250ms by default) + expect( + await client.bzmpop([nonExistingKey], ScoreFilter.MAX, 0.5), + ).toBeNull; + expect( + await client.bzmpop( + [nonExistingKey], + ScoreFilter.MAX, + 0.55, + 1, + ), + ).toBeNull; + + // key exists, but it is not a sorted set + expect(await client.set(stringKey, "value")).toEqual("OK"); + await expect( + client.bzmpop([stringKey], ScoreFilter.MAX, 0.1), + ).rejects.toThrow(RequestError); + await expect( + client.bzmpop([stringKey], ScoreFilter.MAX, 0.1, 1), + ).rejects.toThrow(RequestError); + + // incorrect argument: key list should not be empty + await expect( + client.bzmpop([], ScoreFilter.MAX, 0.1, 1), + ).rejects.toThrow(RequestError); + + // incorrect argument: count should be greater than 0 + await expect( + client.bzmpop([key1], ScoreFilter.MAX, 0.1, 0), + ).rejects.toThrow(RequestError); + + // incorrect argument: timeout can not be a negative number + await expect( + client.bzmpop([key1], ScoreFilter.MAX, -1, 10), + ).rejects.toThrow(RequestError); + + // check that order of entries in the response is preserved + const entries: Record = {}; + + for (let i = 0; i < 10; i++) { + // a0 => 0, a1 => 1 etc + entries["a" + i] = i; + } + + expect(await client.zadd(key2, entries)).toEqual(10); + const result = await client.bzmpop( + [key2], + ScoreFilter.MIN, + 0.1, + 10, + ); + + if (result) { + expect(result[1]).toEqual(entries); + } + + // TODO: add test case with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `geodist test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 9f23326bc6..17ce37c6f6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -667,6 +667,18 @@ export async function transactionTest( responseData.push(["zmpop([key14], MAX)", [key14, { two: 2.0 }]]); baseTransaction.zmpop([key14], ScoreFilter.MAX, 1); responseData.push(["zmpop([key14], MAX, 1)", [key14, { one: 1.0 }]]); + baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); + responseData.push(["zadd(key14, { one: 1.0, two: 2.0 })", 2]); + baseTransaction.bzmpop([key14], ScoreFilter.MAX, 0.1); + responseData.push([ + "bzmpop([key14], ScoreFilter.MAX, 0.1)", + [key14, { two: 2.0 }], + ]); + baseTransaction.bzmpop([key14], ScoreFilter.MAX, 0.1, 1); + responseData.push([ + "bzmpop([key14], ScoreFilter.MAX, 0.1, 1)", + [key14, { one: 1.0 }], + ]); } baseTransaction.xadd(key9, [["field", "value1"]], { id: "0-1" }); From cc1c2e6d2d95e960a69877ebb05883572411fae7 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:52:33 -0700 Subject: [PATCH 057/236] Update documentation to use valkey-glide version 1.x.x (#2021) Signed-off-by: Jonathan Louie --- java/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/java/README.md b/java/README.md index aaa29dd0be..d31048b22a 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.1', classifier: 'osx-aarch_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'osx-aarch_64' } // osx-x86_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'osx-x86_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'osx-x86_64' } // linux-aarch_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-aarch_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'linux-aarch_64' } // linux-x86_64 dependencies { - implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-x86_64' + implementation group: 'io.valkey', name: 'valkey-glide', version: '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.1', classifier: osdetector.classifier + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: osdetector.classifier } ``` @@ -102,7 +102,7 @@ Maven: io.valkey valkey-glide osx-aarch_64 - 1.0.1 + [1.0.0,2.0.0) @@ -110,7 +110,7 @@ Maven: io.valkey valkey-glide osx-x86_64 - 1.0.1 + [1.0.0,2.0.0) @@ -118,7 +118,7 @@ Maven: io.valkey valkey-glide linux-aarch_64 - 1.0.1 + [1.0.0,2.0.0) @@ -126,7 +126,7 @@ Maven: io.valkey valkey-glide linux-x86_64 - 1.0.1 + [1.0.0,2.0.0) ``` From 0ed4c6b1eee933c6857eb6cc592d5b00c5fdeec4 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 25 Jul 2024 11:12:44 -0700 Subject: [PATCH 058/236] current work Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 41 +++++++++++++++++++++++++++++++++++++++++ node/src/Commands.ts | 19 +++++++++++++++++++ node/src/Transaction.ts | 21 +++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ef25d8be0e..e9f2fa5647 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1589,6 +1589,47 @@ export class BaseClient { ); } + /** + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `BLMOVE` is the blocking variant of `LMOVE`. + + Note: + 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. + See https://valkey.io/commands/blmove/ for details. + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + Returns: + Optional[str]: The popped element, or None if `source` does not exist or if the operation timed-out. + Examples: + >>> await client.lpush("testKey1", ["two", "one"]) + >>> await client.lpush("testKey2", ["four", "three"]) + >>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1) + "one" + >>> await client.lrange("testKey1", 0, -1) + ["two"] + >>> updated_array2 = await client.lrange("testKey2", 0, -1) + ["one", "three", "four"] + Since: Redis version 6.2.0. + */ + public async blmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + timeout: number, + ): Promise { + return this.createWritePromise( + createBLMove(source, destination, whereFrom, whereTo, timeout), + ); + } + /** * 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. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 4c1c6254c3..bf5a00bdf9 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -615,6 +615,25 @@ export function createLMove( ]); } +/** + * @internal + */ +export function createBLMove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + timeout: number +): command_request.Command { + return createCommand(RequestType.LMove, [ + source, + destination, + whereFrom, + whereTo, + number + ]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b945655cbb..d60c8ba86a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -82,6 +82,7 @@ import { createLInsert, createLLen, createLMove, + createBLMove createLPop, createLPos, createLPush, @@ -793,6 +794,26 @@ export class BaseTransaction> { ); } + /** + * + * Blocks the connection until it pops atomically and removes the left/right-most element to the + * list stored at `source` depending on `whereFrom`, and pushes the element at the first/last element + * of the list stored at `destination` depending on `whereTo`. + * `BLMOVE` is the blocking variant of `LMOVE`. + */ + public blmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + timeout: number, + + ): T { + return this.addAndReturn( + createLMove(source, destination, whereFrom, whereTo, timeout), + ); + } + /** * 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. From db6c1d9888e8a459fbf94fc9dc2080de3256f904 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 11:55:23 -0700 Subject: [PATCH 059/236] fox timeout error Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 62 +++++++++-------- node/src/Commands.ts | 6 +- node/src/Transaction.ts | 21 +++++- node/tests/SharedTests.ts | 138 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 33 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e9f2fa5647..d8d85a1c9a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -72,6 +72,7 @@ import { createLInsert, createLLen, createLMove, + createBLMove, createLPop, createLPos, createLPush, @@ -1590,33 +1591,40 @@ export class BaseClient { } /** - Blocks the connection until it pops atomically and removes the left/right-most element to the - list stored at `source` depending on `where_from`, and pushes the element at the first/last element - of the list stored at `destination` depending on `where_to`. - `BLMOVE` is the blocking variant of `LMOVE`. - - Note: - 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. - 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. - See https://valkey.io/commands/blmove/ for details. - Args: - source (str): The key to the source list. - destination (str): The key to the destination list. - where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). - where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). - timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. - Returns: - Optional[str]: The popped element, or None if `source` does not exist or if the operation timed-out. - Examples: - >>> await client.lpush("testKey1", ["two", "one"]) - >>> await client.lpush("testKey2", ["four", "three"]) - >>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1) - "one" - >>> await client.lrange("testKey1", 0, -1) - ["two"] - >>> updated_array2 = await client.lrange("testKey2", 0, -1) - ["one", "three", "four"] - Since: Redis version 6.2.0. + * Blocks the connection until it pops atomically and removes the left/right-most element to the + * list stored at `source` depending on `whereFrom`, and pushes the element at the first/last element + * of the list stored at `destination` depending on `whereTo`. + * `BLMOVE` is the blocking variant of `LMOVE`. + * + * Note: + * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. + * + * See https://valkey.io/commands/blmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. + * + * + * Since: Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.lpush("testKey1", ["two", "one"]); + * await client.lpush("testKey2", ["four", "three"]); + * const result = await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1); + * console.log(result); // Output: "one" + * + * const result2 = await client.lrange("testKey1", 0, -1); + * console.log(result2); // Output: "two" + * + * const updated_array2 = await client.lrange("testKey2", 0, -1); + * console.log(updated_array2); // Output: "one", "three", "four"] + * ``` */ public async blmove( source: string, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index bf5a00bdf9..1c42e043fc 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -623,14 +623,14 @@ export function createBLMove( destination: string, whereFrom: ListDirection, whereTo: ListDirection, - timeout: number + timeout: number, ): command_request.Command { - return createCommand(RequestType.LMove, [ + return createCommand(RequestType.BLMove, [ source, destination, whereFrom, whereTo, - number + timeout.toString(), ]); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index d60c8ba86a..62a6a55f56 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -82,7 +82,7 @@ import { createLInsert, createLLen, createLMove, - createBLMove + createBLMove, createLPop, createLPos, createLPush, @@ -800,6 +800,22 @@ export class BaseTransaction> { * list stored at `source` depending on `whereFrom`, and pushes the element at the first/last element * of the list stored at `destination` depending on `whereTo`. * `BLMOVE` is the blocking variant of `LMOVE`. + * + * Note: + * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. + * + * See https://valkey.io/commands/blmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + * + * Command Response - The popped element, or `null` if `source` does not exist or if the operation timed-out. + * + * Since: Valkey version 6.2.0. */ public blmove( source: string, @@ -807,10 +823,9 @@ export class BaseTransaction> { whereFrom: ListDirection, whereTo: ListDirection, timeout: number, - ): T { return this.addAndReturn( - createLMove(source, destination, whereFrom, whereTo, timeout), + createBLMove(source, destination, whereFrom, whereTo, timeout), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 81fb10e214..8d9abbe823 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1334,6 +1334,144 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `blmove list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const lpushArgs1 = ["2", "1"]; + const lpushArgs2 = ["4", "3"]; + + // Initialize the tests + expect(await client.lpush(key1, lpushArgs1)).toEqual(2); + expect(await client.lpush(key2, lpushArgs2)).toEqual(2); + + // Move from LEFT to LEFT with blocking + checkSimple( + await client.blmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ), + ).toEqual("1"); + + // Move from LEFT to RIGHT with blocking + checkSimple( + await client.blmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0.1, + ), + ).toEqual("2"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + "4", + "2", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([]); + + // Move from RIGHT to LEFT non-existing destination with blocking + checkSimple( + await client.blmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.LEFT, + 0.1, + ), + ).toEqual("2"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + "4", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual(["2"]); + + // Move from RIGHT to RIGHT with blocking + checkSimple( + await client.blmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.RIGHT, + 0.1, + ), + ).toEqual("4"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "2", + "4", + ]); + + // Non-existing source key with blocking + expect( + await client.blmove( + "{key}-non_existing_key" + uuidv4(), + key1, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ), + ).toEqual(null); + + // Non-list source key with blocking + const key3 = "{key}-3" + uuidv4(); + checkSimple(await client.set(key3, "value")).toEqual("OK"); + await expect( + client.blmove( + key3, + key1, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ), + ).rejects.toThrow(RequestError); + + // Non-list destination key + await expect( + client.blmove( + key1, + key3, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ), + ).rejects.toThrow(RequestError); + + // BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + // avoid having the test block forever + if (client instanceof GlideClient) { + expect( + await client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, + ), + ).rejects.toThrow(); + } + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lset test_%p`, async (protocol) => { From 0672ea3f6d0494d7229b1a86c8204492a97c86fc Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:55:21 -0700 Subject: [PATCH 060/236] Add command HStrlen (#2020) * Add command HStrlen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 21 +++++++++++++++++++++ node/src/Commands.ts | 10 ++++++++++ node/src/Transaction.ts | 15 +++++++++++++++ node/tests/SharedTests.ts | 27 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e2e3cf99..9c129c304c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ * Node: Added ZRevRank command ([#1977](https://github.com/valkey-io/valkey-glide/pull/1977)) * Node: Added GeoDist command ([#1988](https://github.com/valkey-io/valkey-glide/pull/1988)) * Node: Added GeoHash command ([#1997](https://github.com/valkey-io/valkey-glide/pull/1997)) +* Node: Added HStrlen command ([#2020](https://github.com/valkey-io/valkey-glide/pull/2020)) #### 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/BaseClient.ts b/node/src/BaseClient.ts index ef25d8be0e..636b352825 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -64,6 +64,7 @@ import { createHMGet, createHSet, createHSetNX, + createHStrlen, createHVals, createIncr, createIncrBy, @@ -1393,6 +1394,26 @@ export class BaseClient { return this.createWritePromise(createHVals(key)); } + /** + * Returns the string length of the value associated with `field` in the hash stored at `key`. + * + * See https://valkey.io/commands/hstrlen/ for details. + * + * @param key - The key of the hash. + * @param field - The field in the hash. + * @returns The string length or `0` if `field` or `key` does not exist. + * + * @example + * ```typescript + * await client.hset("my_hash", {"field": "value"}); + * const result = await client.hstrlen("my_hash", "field"); + * console.log(result); // Output: 5 + * ``` + */ + public hstrlen(key: string, field: string): Promise { + return this.createWritePromise(createHStrlen(key, field)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 4c1c6254c3..d67e6031f9 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2249,3 +2249,13 @@ export function createZIncrBy( member, ]); } + +/** + * @internal + */ +export function createHStrlen( + key: string, + field: string, +): command_request.Command { + return createCommand(RequestType.HStrlen, [key, field]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b945655cbb..8c8f391b24 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -73,6 +73,7 @@ import { createHMGet, createHSet, createHSetNX, + createHStrlen, createHVals, createIncr, createIncrBy, @@ -680,6 +681,20 @@ export class BaseTransaction> { return this.addAndReturn(createHVals(key)); } + /** + * Returns the string length of the value associated with `field` in the hash stored at `key`. + * + * See https://valkey.io/commands/hstrlen/ for details. + * + * @param key - The key of the hash. + * @param field - The field in the hash. + * + * Command Response - The string length or `0` if `field` or `key` does not exist. + */ + public hstrlen(key: string, field: string): T { + return this.addAndReturn(createHStrlen(key, field)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 81fb10e214..3e1fba7147 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1102,6 +1102,33 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `hstrlen test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const field = uuidv4(); + + expect(await client.hset(key1, { field: "value" })).toBe(1); + expect(await client.hstrlen(key1, "field")).toBe(5); + + // missing value + expect(await client.hstrlen(key1, "nonExistingField")).toBe(0); + + // missing key + expect(await client.hstrlen(key2, "field")).toBe(0); + + // key exists but holds non hash type value + checkSimple(await client.set(key2, "value")).toEqual("OK"); + await expect(client.hstrlen(key2, field)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lpush, lpop and lrange with existing and non existing key_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 17ce37c6f6..e589213359 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -439,6 +439,8 @@ export async function transactionTest( responseData.push(["del([key1])", 1]); baseTransaction.hset(key4, { [field]: value }); responseData.push(["hset(key4, { [field]: value })", 1]); + baseTransaction.hstrlen(key4, field); + responseData.push(["hstrlen(key4, field)", value.length]); baseTransaction.hlen(key4); responseData.push(["hlen(key4)", 1]); baseTransaction.hsetnx(key4, field, value); From 04ece0135c89a56eb3745bfb5d86fd9ede40d6fc Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 14:17:23 -0700 Subject: [PATCH 061/236] implement transacton Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 48 +++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 8d9abbe823..3fcf700d95 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,6 +29,7 @@ import { Script, UpdateByScore, parseInfoResponse, + TimeoutError, } from "../"; import { RedisCluster } from "../../utils/TestUtils"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; @@ -1456,17 +1457,44 @@ export function runBaseTests(config: { // BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to // avoid having the test block forever - if (client instanceof GlideClient) { - expect( - await client.blmove( - "{SameSlot}non_existing_key", - key2, - ListDirection.LEFT, - ListDirection.RIGHT, - 0, - ), - ).rejects.toThrow(); + + // async setTimeout(() => { + // if (client instanceof GlideClient) { + // expect( + // await client.blmove( + // "{SameSlot}non_existing_key", + // key2, + // ListDirection.LEFT, + // ListDirection.RIGHT, + // 0, + // ), + // ).rejects.toThrow(TimeoutError); + // } + // }, 10000); + + // Helper function to wrap setTimeout in a promise + function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); } + + // Async function using delay + async function blmove_timeout_test() { + await delay(10000); // Wait for 10 seconds + + if (client instanceof GlideClusterClient) { + expect( + await client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, + ), + ).rejects.toThrow(TimeoutError); + } + } + + blmove_timeout_test(); }, protocol); }, config.timeout, From 6885e03e2928b670e055c62c6632bba44b039f31 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 14:49:29 -0700 Subject: [PATCH 062/236] run linter Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 3fcf700d95..ba7ea777b9 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1459,20 +1459,19 @@ export function runBaseTests(config: { // avoid having the test block forever // async setTimeout(() => { - // if (client instanceof GlideClient) { - // expect( - // await client.blmove( - // "{SameSlot}non_existing_key", - // key2, - // ListDirection.LEFT, - // ListDirection.RIGHT, - // 0, - // ), - // ).rejects.toThrow(TimeoutError); - // } + // if (client instanceof GlideClient) { + // expect( + // await client.blmove( + // "{SameSlot}non_existing_key", + // key2, + // ListDirection.LEFT, + // ListDirection.RIGHT, + // 0, + // ), + // ).rejects.toThrow(TimeoutError); + // } // }, 10000); - // Helper function to wrap setTimeout in a promise function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } From fc22dfeb135babf275f645375ddeb6be201caae1 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 14:53:55 -0700 Subject: [PATCH 063/236] add changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e2e3cf99..dc96030d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) From cafb3ebfdcc803b5b3c577623758dfd9992085fd Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 15:28:11 -0700 Subject: [PATCH 064/236] fixed shared test Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 43 +++++++++------------------------------ 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ba7ea777b9..b8dfdbe7d0 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1457,40 +1457,17 @@ export function runBaseTests(config: { // BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to // avoid having the test block forever - - // async setTimeout(() => { - // if (client instanceof GlideClient) { - // expect( - // await client.blmove( - // "{SameSlot}non_existing_key", - // key2, - // ListDirection.LEFT, - // ListDirection.RIGHT, - // 0, - // ), - // ).rejects.toThrow(TimeoutError); - // } - // }, 10000); - - function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - // Async function using delay async function blmove_timeout_test() { - await delay(10000); // Wait for 10 seconds - - if (client instanceof GlideClusterClient) { - expect( - await client.blmove( - "{SameSlot}non_existing_key", - key2, - ListDirection.LEFT, - ListDirection.RIGHT, - 0, - ), - ).rejects.toThrow(TimeoutError); - } + await wait(50000); + expect( + await client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, + ), + ).rejects.toThrow(TimeoutError); } blmove_timeout_test(); From 7bad778dc4cf07ad811d2ed7cb1712226ff4f040 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 16:53:02 -0700 Subject: [PATCH 065/236] implement blmove Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 12 ++++++++---- node/tests/TestUtilities.ts | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b8dfdbe7d0..5ccca21e7e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1459,18 +1459,22 @@ export function runBaseTests(config: { // avoid having the test block forever async function blmove_timeout_test() { await wait(50000); - expect( - await client.blmove( + await expect( + client.blmove( "{SameSlot}non_existing_key", key2, ListDirection.LEFT, ListDirection.RIGHT, 0, ), - ).rejects.toThrow(TimeoutError); + ).rejects.toThrow(ClosingError); } - blmove_timeout_test(); + try { + blmove_timeout_test(); + } catch (ClosingError) { + console.log("Closing error with timeout occurred."); + } }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 17ce37c6f6..1f8f81a12f 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -487,13 +487,22 @@ export async function transactionTest( field + "3", ]); - baseTransaction.lpopCount(key5, 2); - responseData.push(["lpopCount(key5, 2)", [field + "2"]]); - } else { - baseTransaction.lpopCount(key5, 2); - responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + baseTransaction.blmove( + key20, + key5, + ListDirection.LEFT, + ListDirection.LEFT, + 3, + ); + responseData.push([ + "blmove(key20, key5, ListDirection.LEFT, ListDirection.LEFT, 3)", + field + "3", + ]); } + baseTransaction.lpopCount(key5, 2); + responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + baseTransaction.linsert( key5, InsertPosition.Before, From c1ed2ccaff67962db695b7802ccedb525f53ebc7 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 16:56:14 -0700 Subject: [PATCH 066/236] removed timeout error Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5ccca21e7e..828988ef7e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,7 +29,6 @@ import { Script, UpdateByScore, parseInfoResponse, - TimeoutError, } from "../"; import { RedisCluster } from "../../utils/TestUtils"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; From 4a9e9dfb08de71b52e19b8dd9c24373216924c32 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 26 Jul 2024 17:16:31 -0700 Subject: [PATCH 067/236] remove extra line Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d8d85a1c9a..9d463c0282 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1609,7 +1609,6 @@ export class BaseClient { * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. * - * * Since: Valkey version 6.2.0. * * @example From b753216b30b4fcfe273d46c1add9f53d523129cc Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 26 Jul 2024 17:29:10 -0700 Subject: [PATCH 068/236] Node: add `GEOSEARCH` (#2007) * Add `GEOSEARCH` Signed-off-by: Yury-Fridlyand Co-authored-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 18 +++ node/src/BaseClient.ts | 96 +++++++++++++ node/src/Commands.ts | 130 +++++++++++++++-- node/src/Transaction.ts | 54 +++++++ node/tests/SharedTests.ts | 277 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 90 ++++++++++++ 7 files changed, 657 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c129c304c..ffc5134eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007)) * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 16585715f3..5c52a70f06 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -79,6 +79,15 @@ function initialize() { BitwiseOperation, ConditionalChange, GeoAddOptions, + CoordOrigin, + MemberOrigin, + SearchOrigin, + GeoBoxShape, + GeoCircleShape, + GeoSearchShape, + GeoSearchResultOptions, + SortOrder, + GeoUnit, GeospatialData, GlideClient, GlideClusterClient, @@ -136,6 +145,15 @@ function initialize() { BitwiseOperation, ConditionalChange, GeoAddOptions, + CoordOrigin, + MemberOrigin, + SearchOrigin, + GeoBoxShape, + GeoCircleShape, + GeoSearchShape, + GeoSearchResultOptions, + SortOrder, + GeoUnit, GeospatialData, GlideClient, GlideClusterClient, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 636b352825..bdf192a33a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -15,12 +15,18 @@ import { BitmapIndexType, BitOffsetOptions, BitwiseOperation, + CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, GeoAddOptions, + GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars + GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars + GeoSearchResultOptions, + GeoSearchShape, GeospatialData, GeoUnit, InsertPosition, KeyWeight, + MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars LPosOptions, ListDirection, RangeByIndex, @@ -28,6 +34,7 @@ import { RangeByScore, ScoreBoundary, ScoreFilter, + SearchOrigin, SetOptions, StreamAddOptions, StreamReadOptions, @@ -51,6 +58,7 @@ import { createGeoDist, createGeoHash, createGeoPos, + createGeoSearch, createGet, createGetBit, createGetDel, @@ -3660,6 +3668,94 @@ export class BaseClient { ); } + /** + * Returns the members of a sorted set populated with geospatial information using {@link geoadd}, + * which are within the borders of the area specified by a given shape. + * + * See https://valkey.io/commands/geosearch/ for more details. + * + * since - Valkey 6.2.0 and above. + * + * @param key - The key of the sorted set. + * @param searchFrom - The query's center point options, could be one of: + * + * - {@link MemberOrigin} to use the position of the given existing member in the sorted set. + * + * - {@link CoordOrigin} to use the given longitude and latitude coordinates. + * + * @param searchBy - The query's shape options, could be one of: + * + * - {@link GeoCircleShape} to search inside circular area according to given radius. + * + * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. + * + * @param resultOptions - The optional inputs to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}. + * @returns By default, returns an `Array` of members (locations) names. + * If any of `withCoord`, `withDist` or `withHash` are set to `true` in {@link GeoSearchResultOptions}, a 2D `Array` returned, + * where each sub-array represents a single item in the following order: + * + * - The member (location) name. + * + * - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`. + * + * - The geohash of the location as a integer `number`, if `withHash` is set to `true`. + * + * - The coordinates as a two item `array` of floating point `number`s, if `withCoord` is set to `true`. + * + * @example + * ```typescript + * const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]); + * await client.geoadd("mySortedSet", data); + * // search for locations within 200 km circle around stored member named 'Palermo' + * const result1 = await client.geosearch("mySortedSet", { member: "Palermo" }, { radius: 200, unit: GeoUnit.KILOMETERS }); + * console.log(result1); // Output: ['Palermo', 'Catania'] + * + * // search for locations in 200x300 mi rectangle centered at coordinate (15, 37), requesting additional info, + * // limiting results by 2 best matches, ordered by ascending distance from the search area center + * const result2 = await client.geosearch( + * "mySortedSet", + * { position: { longitude: 15, latitude: 37 } }, + * { width: 200, height: 300, unit: GeoUnit.MILES }, + * { + * sortOrder: SortOrder.ASC, + * count: 2, + * withCoord: true, + * withDist: true, + * withHash: true, + * }, + * ); + * console.log(result2); // Output: + * // [ + * // [ + * // 'Catania', // location name + * // [ + * // 56.4413, // distance + * // 3479447370796909, // geohash of the location + * // [15.087267458438873, 37.50266842333162], // coordinates of the location + * // ], + * // ], + * // [ + * // 'Palermo', + * // [ + * // 190.4424, + * // 3479099956230698, + * // [13.361389338970184, 38.1155563954963], + * // ], + * // ], + * // ] + * ``` + */ + public async geosearch( + key: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchResultOptions, + ): Promise<(Buffer | (number | number[])[])[]> { + return this.createWritePromise( + createGeoSearch(key, searchFrom, searchBy, resultOptions), + ); + } + /** * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d67e6031f9..95d51f6905 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2096,28 +2096,23 @@ export function createGeoAdd( } membersToGeospatialData.forEach((coord, member) => { - args = args.concat([ + args = args.concat( coord.longitude.toString(), coord.latitude.toString(), - ]); - args.push(member); + member, + ); }); return createCommand(RequestType.GeoAdd, args); } -/** - * Enumeration representing distance units options for the {@link geodist} command. - */ +/** Enumeration representing distance units options. */ export enum GeoUnit { /** Represents distance in meters. */ METERS = "m", - /** Represents distance in kilometers. */ KILOMETERS = "km", - /** Represents distance in miles. */ MILES = "mi", - /** Represents distance in feet. */ FEET = "ft", } @@ -2161,6 +2156,123 @@ export function createGeoHash( return createCommand(RequestType.GeoHash, args); } +/** + * Optional parameters for {@link BaseClient.geosearch|geosearch} command which defines what should be included in the + * search results and how results should be ordered and limited. + */ +export type GeoSearchResultOptions = { + /** Include the coordinate of the returned items. */ + withCoord?: boolean; + /** + * Include the distance of the returned items from the specified center point. + * The distance is returned in the same unit as specified for the `searchBy` argument. + */ + withDist?: boolean; + /** Include the geohash of the returned items. */ + withHash?: boolean; + /** Indicates the order the result should be sorted in. */ + sortOrder?: SortOrder; + /** Indicates the number of matches the result should be limited to. */ + count?: number; + /** Whether to allow returning as enough matches are found. This requires `count` parameter to be set. */ + isAny?: boolean; +}; + +/** Defines the sort order for nested results. */ +export enum SortOrder { + /** Sort by ascending order. */ + ASC = "ASC", + /** Sort by descending order. */ + DESC = "DESC", +} + +export type GeoSearchShape = GeoCircleShape | GeoBoxShape; + +/** Circle search shape defined by the radius value and measurement unit. */ +export type GeoCircleShape = { + /** The radius to search by. */ + radius: number; + /** The measurement unit of the radius. */ + unit: GeoUnit; +}; + +/** Rectangle search shape defined by the width and height and measurement unit. */ +export type GeoBoxShape = { + /** The width of the rectangle to search by. */ + width: number; + /** The height of the rectangle to search by. */ + height: number; + /** The measurement unit of the width and height. */ + unit: GeoUnit; +}; + +export type SearchOrigin = CoordOrigin | MemberOrigin; + +/** The search origin represented by a {@link GeospatialData} position. */ +export type CoordOrigin = { + /** The pivot location to search from. */ + position: GeospatialData; +}; + +/** The search origin represented by an existing member. */ +export type MemberOrigin = { + /** Member (location) name stored in the sorted set to use as a search pivot. */ + member: string; +}; + +/** + * @internal + */ +export function createGeoSearch( + key: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchResultOptions, +): command_request.Command { + let args: string[] = [key]; + + if ("position" in searchFrom) { + args = args.concat( + "FROMLONLAT", + searchFrom.position.longitude.toString(), + searchFrom.position.latitude.toString(), + ); + } else { + args = args.concat("FROMMEMBER", searchFrom.member); + } + + if ("radius" in searchBy) { + args = args.concat( + "BYRADIUS", + searchBy.radius.toString(), + searchBy.unit, + ); + } else { + args = args.concat( + "BYBOX", + searchBy.width.toString(), + searchBy.height.toString(), + searchBy.unit, + ); + } + + if (resultOptions) { + if (resultOptions.withCoord) args.push("WITHCOORD"); + if (resultOptions.withDist) args.push("WITHDIST"); + if (resultOptions.withHash) args.push("WITHHASH"); + + if (resultOptions.count) { + args.push("COUNT", resultOptions.count?.toString()); + + if (resultOptions.isAny) args.push("ANY"); + } + + if (resultOptions.sortOrder) args.push(resultOptions.sortOrder); + } + + return createCommand(RequestType.GeoSearch, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8c8f391b24..9c7854b846 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -7,9 +7,14 @@ import { BitOffsetOptions, BitmapIndexType, BitwiseOperation, + CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, FlushMode, GeoAddOptions, + GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars + GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars + GeoSearchResultOptions, + GeoSearchShape, GeospatialData, GeoUnit, InfoOptions, @@ -18,11 +23,13 @@ import { LPosOptions, ListDirection, LolwutOptions, + MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars RangeByIndex, RangeByLex, RangeByScore, ScoreBoundary, ScoreFilter, + SearchOrigin, SetOptions, StreamAddOptions, StreamReadOptions, @@ -60,6 +67,7 @@ import { createGeoDist, createGeoHash, createGeoPos, + createGeoSearch, createGet, createGetBit, createGetDel, @@ -2160,6 +2168,52 @@ export class BaseTransaction> { ); } + /** + * Returns the members of a sorted set populated with geospatial information using {@link geoadd}, + * which are within the borders of the area specified by a given shape. + * + * See https://valkey.io/commands/geosearch/ for more details. + * + * since - Valkey 6.2.0 and above. + * + * @param key - The key of the sorted set. + * @param searchFrom - The query's center point options, could be one of: + * + * - {@link MemberOrigin} to use the position of the given existing member in the sorted set. + * + * - {@link CoordOrigin} to use the given longitude and latitude coordinates. + * + * @param searchBy - The query's shape options, could be one of: + * + * - {@link GeoCircleShape} to search inside circular area according to given radius. + * + * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. + * + * @param resultOptions - The optional inputs to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}. + * + * Command Response - By default, returns an `Array` of members (locations) names. + * If any of `withCoord`, `withDist` or `withHash` are set to `true` in {@link GeoSearchResultOptions}, a 2D `Array` returned, + * where each sub-array represents a single item in the following order: + * + * - The member (location) name. + * + * - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`. + * + * - The geohash of the location as a integer `number`. + * + * - The coordinates as a two item `array` of floating point `number`s. + */ + public geosearch( + key: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchResultOptions, + ): T { + return this.addAndReturn( + createGeoSearch(key, searchFrom, searchBy, resultOptions), + ); + } + /** * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 3e1fba7147..203136507b 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -27,6 +27,7 @@ import { RequestError, ScoreFilter, Script, + SortOrder, UpdateByScore, parseInfoResponse, } from "../"; @@ -4893,6 +4894,282 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geosearch test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const key = uuidv4(); + + const members: string[] = [ + "Catania", + "Palermo", + "edge2", + "edge1", + ]; + const membersSet: Set = new Set(members); + const membersCoordinates: [number, number][] = [ + [15.087269, 37.502669], + [13.361389, 38.115556], + [17.24151, 38.788135], + [12.758489, 38.788135], + ]; + + const membersGeoData: GeospatialData[] = []; + + for (const [lon, lat] of membersCoordinates) { + membersGeoData.push({ longitude: lon, latitude: lat }); + } + + const membersToCoordinates = new Map(); + + for (let i = 0; i < members.length; i++) { + membersToCoordinates.set(members[i], membersGeoData[i]); + } + + const expectedResult = [ + [ + members[0], + [56.4413, 3479447370796909, membersCoordinates[0]], + ], + [ + members[1], + [190.4424, 3479099956230698, membersCoordinates[1]], + ], + [ + members[2], + [279.7403, 3481342659049484, membersCoordinates[2]], + ], + [ + members[3], + [279.7405, 3479273021651468, membersCoordinates[3]], + ], + ]; + + // geoadd + expect(await client.geoadd(key, membersToCoordinates)).toBe( + members.length, + ); + + let searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ); + // using set to compare, because results are reordrered + checkSimple(new Set(searchResult)).toEqual(membersSet); + + // order search result + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { sortOrder: SortOrder.ASC }, + ); + checkSimple(searchResult).toEqual(members); + + // order and query all extra data + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + withCoord: true, + withDist: true, + withHash: true, + }, + ); + checkSimple(searchResult).toEqual(expectedResult); + + // order, query and limit by 1 + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + withCoord: true, + withDist: true, + withHash: true, + count: 1, + }, + ); + checkSimple(searchResult).toEqual(expectedResult.slice(0, 1)); + + // test search by box, unit: meters, from member, with distance + const meters = 400 * 1000; + searchResult = await client.geosearch( + key, + { member: "Catania" }, + { width: meters, height: meters, unit: GeoUnit.METERS }, + { + withDist: true, + withCoord: false, + sortOrder: SortOrder.DESC, + }, + ); + checkSimple(searchResult).toEqual([ + ["edge2", [236529.1799]], + ["Palermo", [166274.1516]], + ["Catania", [0.0]], + ]); + + // test search by box, unit: feet, from member, with limited count 2, with hash + const feet = 400 * 3280.8399; + searchResult = await client.geosearch( + key, + { member: "Palermo" }, + { width: feet, height: feet, unit: GeoUnit.FEET }, + { + withDist: false, + withCoord: false, + withHash: true, + sortOrder: SortOrder.ASC, + count: 2, + }, + ); + checkSimple(searchResult).toEqual([ + ["Palermo", [3479099956230698]], + ["edge1", [3479273021651468]], + ]); + + // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 + const miles = 250; + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: miles, height: miles, unit: GeoUnit.MILES }, + { count: 1, isAny: true }, + ); + expect(members.map((m) => Buffer.from(m))).toContainEqual( + searchResult[0], + ); + + // test search by radius, units: feet, from member + const feetRadius = 200 * 3280.8399; + searchResult = await client.geosearch( + key, + { member: "Catania" }, + { radius: feetRadius, unit: GeoUnit.FEET }, + { sortOrder: SortOrder.ASC }, + ); + checkSimple(searchResult).toEqual(["Catania", "Palermo"]); + + // Test search by radius, unit: meters, from member + const metersRadius = 200 * 1000; + searchResult = await client.geosearch( + key, + { member: "Catania" }, + { radius: metersRadius, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.DESC }, + ); + checkSimple(searchResult).toEqual(["Palermo", "Catania"]); + + searchResult = await client.geosearch( + key, + { member: "Catania" }, + { radius: metersRadius, unit: GeoUnit.METERS }, + { + sortOrder: SortOrder.DESC, + withHash: true, + }, + ); + checkSimple(searchResult).toEqual([ + ["Palermo", [3479099956230698]], + ["Catania", [3479447370796909]], + ]); + + // Test search by radius, unit: miles, from geospatial data + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { radius: 175, unit: GeoUnit.MILES }, + { sortOrder: SortOrder.DESC }, + ); + checkSimple(searchResult).toEqual([ + "edge1", + "edge2", + "Palermo", + "Catania", + ]); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 2, + withHash: true, + withCoord: true, + withDist: true, + }, + ); + checkSimple(searchResult).toEqual(expectedResult.slice(0, 2)); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 1, + isAny: true, + withCoord: true, + withDist: true, + withHash: true, + }, + ); + expect(members.map((m) => Buffer.from(m))).toContainEqual( + searchResult[0][0], + ); + + // no members within the area + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { width: 50, height: 50, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ); + expect(searchResult).toEqual([]); + + // no members within the area + searchResult = await client.geosearch( + key, + { position: { longitude: 15, latitude: 37 } }, + { radius: 5, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ); + expect(searchResult).toEqual([]); + + // member does not exist + await expect( + client.geosearch( + key, + { member: "non-existing-member" }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + + // key exists but holds a non-ZSET value + const key2 = uuidv4(); + expect(await client.set(key2, uuidv4())).toEqual("OK"); + await expect( + client.geosearch( + key2, + { position: { longitude: 15, latitude: 37 } }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zmpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e589213359..02c9eef147 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -24,6 +24,7 @@ import { ProtocolVersion, ReturnType, ScoreFilter, + SortOrder, Transaction, } from ".."; @@ -810,6 +811,95 @@ export async function transactionTest( ["sqc8b49rny0", "sqdtr74hyu0", null], ]); + if (gte("6.2.0", version)) { + baseTransaction + .geosearch( + key18, + { member: "Palermo" }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { sortOrder: SortOrder.ASC }, + ) + .geosearch( + key18, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ) + .geosearch( + key18, + { member: "Palermo" }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 2, + withCoord: true, + withDist: true, + withHash: true, + }, + ) + .geosearch( + key18, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 2, + withCoord: true, + withDist: true, + withHash: true, + }, + ); + responseData.push([ + 'geosearch(key18, "Palermo", R200 KM, ASC)', + ["Palermo", "Catania"], + ]); + responseData.push([ + "geosearch(key18, (15, 37), 400x400 KM, ASC)", + ["Palermo", "Catania"], + ]); + responseData.push([ + 'geosearch(key18, "Palermo", R200 KM, ASC 2 3x true)', + [ + [ + "Palermo", + [ + 0.0, + 3479099956230698, + [13.361389338970184, 38.1155563954963], + ], + ], + [ + "Catania", + [ + 166.2742, + 3479447370796909, + [15.087267458438873, 37.50266842333162], + ], + ], + ], + ]); + responseData.push([ + "geosearch(key18, (15, 37), 400x400 KM, ASC 2 3x true)", + [ + [ + "Catania", + [ + 56.4413, + 3479447370796909, + [15.087267458438873, 37.50266842333162], + ], + ], + [ + "Palermo", + [ + 190.4424, + 3479099956230698, + [13.361389338970184, 38.1155563954963], + ], + ], + ], + ]); + } + const libName = "mylib1C" + uuidv4().replaceAll("-", ""); const funcName = "myfunc1c" + uuidv4().replaceAll("-", ""); const code = generateLuaLibCode( From 2de6b7984b03495fbca4ce2936cb40a03d90ca4c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 26 Jul 2024 18:03:03 -0700 Subject: [PATCH 069/236] Node: Add `FUNCTION LIST` (#2019) * Add `FUNCTION LIST` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 + node/src/Commands.ts | 35 ++++++++ node/src/GlideClient.ts | 38 +++++++++ node/src/GlideClusterClient.ts | 44 ++++++++++ node/src/Transaction.ts | 18 ++++ node/tests/RedisClient.test.ts | 90 +++++++++----------- node/tests/RedisClusterClient.test.ts | 117 ++++++++++++++++---------- node/tests/TestUtilities.ts | 64 ++++++++++++++ 9 files changed, 320 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc5134eef..e4ee3a7d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019)) * Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007)) * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 5c52a70f06..f9f35d8685 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -92,6 +92,8 @@ function initialize() { GlideClient, GlideClusterClient, GlideClientConfiguration, + FunctionListOptions, + FunctionListResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, @@ -158,6 +160,8 @@ function initialize() { GlideClient, GlideClusterClient, GlideClientConfiguration, + FunctionListOptions, + FunctionListResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 95d51f6905..e8377c9e47 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1699,6 +1699,41 @@ export function createFunctionLoad( return createCommand(RequestType.FunctionLoad, args); } +/** Optional arguments for `FUNCTION LIST` command. */ +export type FunctionListOptions = { + /** A wildcard pattern for matching library names. */ + libNamePattern?: string; + /** Specifies whether to request the library code from the server or not. */ + withCode?: boolean; +}; + +/** Type of the response of `FUNCTION LIST` command. */ +export type FunctionListResponse = Record< + string, + string | Record[] +>[]; + +/** + * @internal + */ +export function createFunctionList( + options?: FunctionListOptions, +): command_request.Command { + const args: string[] = []; + + if (options) { + if (options.libNamePattern) { + args.push("LIBRARYNAME", options.libNamePattern); + } + + if (options.withCode) { + args.push("WITHCODE"); + } + } + + return createCommand(RequestType.FunctionList, args); +} + /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index c63715b6df..c05126fc58 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -11,6 +11,8 @@ import { } from "./BaseClient"; import { FlushMode, + FunctionListOptions, + FunctionListResponse, InfoOptions, LolwutOptions, createClientGetName, @@ -26,6 +28,7 @@ import { createFlushDB, createFunctionDelete, createFunctionFlush, + createFunctionList, createFunctionLoad, createInfo, createLolwut, @@ -457,6 +460,41 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionFlush(mode)); } + /** + * Returns information about the functions and libraries. + * + * See https://valkey.io/commands/function-list/ for details. + * + * since Valkey version 7.0.0. + * + * @param options - Parameters to filter and request additional info. + * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. + * + * @example + * ```typescript + * // Request info for specific library including the source code + * const result1 = await client.functionList({ libNamePattern: "myLib*", withCode: true }); + * // Request info for all libraries + * const result2 = await client.functionList(); + * console.log(result2); // Output: + * // [{ + * // "library_name": "myLib5_backup", + * // "engine": "LUA", + * // "functions": [{ + * // "name": "myfunc", + * // "description": null, + * // "flags": [ "no-writes" ], + * // }], + * // "library_code": "#!lua name=myLib5_backup \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + * // }] + * ``` + */ + public async functionList( + options?: FunctionListOptions, + ): Promise { + return this.createWritePromise(createFunctionList(options)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 632132a3e0..10e8d87a38 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -11,6 +11,8 @@ import { } from "./BaseClient"; import { FlushMode, + FunctionListOptions, + FunctionListResponse, InfoOptions, LolwutOptions, createClientGetName, @@ -28,6 +30,7 @@ import { createFlushDB, createFunctionDelete, createFunctionFlush, + createFunctionList, createFunctionLoad, createInfo, createLolwut, @@ -807,6 +810,47 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Returns information about the functions and libraries. + * + * See https://valkey.io/commands/function-list/ for details. + * + * since Valkey version 7.0.0. + * + * @param options - Parameters to filter and request additional info. + * @param route - The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed to a random route. + * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. + * + * @example + * ```typescript + * // Request info for specific library including the source code + * const result1 = await client.functionList({ libNamePattern: "myLib*", withCode: true }); + * // Request info for all libraries + * const result2 = await client.functionList(); + * console.log(result2); // Output: + * // [{ + * // "library_name": "myLib5_backup", + * // "engine": "LUA", + * // "functions": [{ + * // "name": "myfunc", + * // "description": null, + * // "flags": [ "no-writes" ], + * // }], + * // "library_code": "#!lua name=myLib5_backup \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + * // }] + * ``` + */ + public async functionList( + options?: FunctionListOptions, + route?: Routes, + ): Promise> { + return this.createWritePromise( + createFunctionList(options), + toProtobufRoute(route), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 9c7854b846..24be54d0a6 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -10,6 +10,8 @@ import { CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, FlushMode, + FunctionListOptions, + FunctionListResponse, // eslint-disable-line @typescript-eslint/no-unused-vars GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -62,6 +64,7 @@ import { createFlushDB, createFunctionDelete, createFunctionFlush, + createFunctionList, createFunctionLoad, createGeoAdd, createGeoDist, @@ -2069,6 +2072,21 @@ export class BaseTransaction> { return this.addAndReturn(createFunctionFlush(mode)); } + /** + * Returns information about the functions and libraries. + * + * See https://valkey.io/commands/function-list/ for details. + * + * since Valkey version 7.0.0. + * + * @param options - Parameters to filter and request additional info. + * + * Command Response - Info about all or selected libraries and their functions in {@link FunctionListResponse} format. + */ + public functionList(options?: FunctionListOptions): T { + return this.addAndReturn(createFunctionList(options)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index e8326ab797..fb4b3e4bd8 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -18,6 +18,7 @@ import { FlushMode } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { + checkFunctionListResponse, checkSimple, convertStringArrayToBuffer, flushAndCloseClient, @@ -382,16 +383,7 @@ describe("GlideClient", () => { new Map([[funcName, "return args[1]"]]), true, ); - // TODO use commands instead of customCommand once implemented - // verify function does not yet exist - expect( - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]), - ).toEqual([]); + expect(await client.functionList()).toEqual([]); checkSimple(await client.functionLoad(code)).toEqual(libName); @@ -402,7 +394,23 @@ describe("GlideClient", () => { await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); - // TODO verify with FUNCTION LIST + let functionList = await client.functionList({ + libNamePattern: libName, + }); + let expectedDescription = new Map([ + [funcName, null], + ]); + let expectedFlags = new Map([ + [funcName, ["no-writes"]], + ]); + + checkFunctionListResponse( + functionList, + libName, + expectedDescription, + expectedFlags, + ); + // re-load library without replace await expect(client.functionLoad(code)).rejects.toThrow( @@ -428,6 +436,24 @@ describe("GlideClient", () => { libName, ); + functionList = await client.functionList({ withCode: true }); + expectedDescription = new Map([ + [funcName, null], + [func2Name, null], + ]); + expectedFlags = new Map([ + [funcName, ["no-writes"]], + [func2Name, ["no-writes"]], + ]); + + checkFunctionListResponse( + functionList, + libName, + expectedDescription, + expectedFlags, + newCode, + ); + checkSimple( await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); @@ -459,16 +485,8 @@ describe("GlideClient", () => { true, ); - // TODO use commands instead of customCommand once implemented // verify function does not yet exist - expect( - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]), - ).toEqual([]); + expect(await client.functionList()).toEqual([]); checkSimple(await client.functionLoad(code)).toEqual(libName); @@ -480,16 +498,8 @@ describe("GlideClient", () => { "OK", ); - // TODO use commands instead of customCommand once implemented // verify function does not yet exist - expect( - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]), - ).toEqual([]); + expect(await client.functionList()).toEqual([]); // Attempt to re-load library without overwriting to ensure FLUSH was effective checkSimple(await client.functionLoad(code)).toEqual(libName); @@ -517,32 +527,16 @@ describe("GlideClient", () => { new Map([[funcName, "return args[1]"]]), true, ); - // TODO use commands instead of customCommand once implemented // verify function does not yet exist - expect( - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]), - ).toEqual([]); + expect(await client.functionList()).toEqual([]); checkSimple(await client.functionLoad(code)).toEqual(libName); // Delete the function expect(await client.functionDelete(libName)).toEqual("OK"); - // TODO use commands instead of customCommand once implemented - // verify function does not exist - expect( - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]), - ).toEqual([]); + // verify function does not yet exist + expect(await client.functionList()).toEqual([]); // deleting a non-existing library await expect(client.functionDelete(libName)).rejects.toThrow( diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index b60f064b69..9b29d6196a 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from "uuid"; import { BitwiseOperation, ClusterTransaction, + FunctionListResponse, GlideClusterClient, InfoOptions, ProtocolVersion, @@ -26,6 +27,7 @@ import { RedisCluster } from "../../utils/TestUtils.js"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, + checkFunctionListResponse, checkSimple, flushAndCloseClient, generateLuaLibCode, @@ -572,7 +574,7 @@ describe("GlideClusterClient", () => { "Single node route = %s", (singleNodeRoute) => { it( - "function load", + "function load and function list", async () => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; @@ -598,15 +600,10 @@ describe("GlideClusterClient", () => { const route: Routes = singleNodeRoute ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; - // TODO use commands instead of customCommand once implemented - // verify function does not yet exist - const functionList = await client.customCommand( - [ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ], + + let functionList = await client.functionList( + { libNamePattern: libName }, + route, ); checkClusterResponse( functionList as object, @@ -617,6 +614,31 @@ describe("GlideClusterClient", () => { checkSimple( await client.functionLoad(code), ).toEqual(libName); + + functionList = await client.functionList( + { libNamePattern: libName }, + route, + ); + let expectedDescription = new Map< + string, + string | null + >([[funcName, null]]); + let expectedFlags = new Map([ + [funcName, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + ), + ); + // call functions from that library to confirm that it works let fcall = await client.fcallWithRoute( funcName, @@ -668,6 +690,35 @@ describe("GlideClusterClient", () => { await client.functionLoad(newCode, true), ).toEqual(libName); + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); + expectedDescription = new Map< + string, + string | null + >([ + [funcName, null], + [func2Name, null], + ]); + expectedFlags = new Map([ + [funcName, ["no-writes"]], + [func2Name, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + newCode, + ), + ); + fcall = await client.fcallWithRoute( func2Name, ["one", "two"], @@ -737,15 +788,10 @@ describe("GlideClusterClient", () => { ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; - // TODO use commands instead of customCommand once implemented - // verify function does not yet exist - const functionList1 = - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]); + const functionList1 = await client.functionList( + {}, + route, + ); checkClusterResponse( functionList1 as object, singleNodeRoute, @@ -775,15 +821,8 @@ describe("GlideClusterClient", () => { ), ).toEqual("OK"); - // TODO use commands instead of customCommand once implemented - // verify function does not exist const functionList2 = - await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]); + await client.functionList(); checkClusterResponse( functionList2 as object, singleNodeRoute, @@ -845,14 +884,10 @@ describe("GlideClusterClient", () => { const route: Routes = singleNodeRoute ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; - // TODO use commands instead of customCommand once implemented - // verify function does not yet exist - let functionList = await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]); + let functionList = await client.functionList( + {}, + route, + ); checkClusterResponse( functionList as object, singleNodeRoute, @@ -872,14 +907,10 @@ describe("GlideClusterClient", () => { await client.functionDelete(libName, route), ).toEqual("OK"); - // TODO use commands instead of customCommand once implemented - // verify function does not exist - functionList = await client.customCommand([ - "FUNCTION", - "LIST", - "LIBRARYNAME", - libName, - ]); + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); checkClusterResponse( functionList as object, singleNodeRoute, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 02c9eef147..3f0b25e000 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -14,6 +14,7 @@ import { BitwiseOperation, ClusterTransaction, FlushMode, + FunctionListResponse, GeoUnit, GeospatialData, GlideClient, @@ -340,6 +341,62 @@ export function compareMaps( return JSON.stringify(map) == JSON.stringify(map2); } +/** + * Validate whether `FUNCTION LIST` response contains required info. + * + * @param response - The response from server. + * @param libName - Expected library name. + * @param functionDescriptions - Expected function descriptions. Key - function name, value - description. + * @param functionFlags - Expected function flags. Key - function name, value - flags set. + * @param libCode - Expected library to check if given. + */ +export function checkFunctionListResponse( + response: FunctionListResponse, + libName: string, + functionDescriptions: Map, + functionFlags: Map, + libCode?: string, +) { + // TODO rework after #1953 https://github.com/valkey-io/valkey-glide/pull/1953 + expect(response.length).toBeGreaterThan(0); + let hasLib = false; + + for (const lib of response) { + hasLib = lib["library_name"] == libName; + + if (hasLib) { + const functions = lib["functions"]; + expect(functions.length).toEqual(functionDescriptions.size); + + for (const functionData of functions) { + const functionInfo = functionData as Record< + string, + string | string[] + >; + const name = ( + functionInfo["name"] as unknown as Buffer + ).toString(); // not a string - suprise + const flags = ( + functionInfo["flags"] as unknown as Buffer[] + ).map((f) => f.toString()); + checkSimple(functionInfo["description"]).toEqual( + functionDescriptions.get(name), + ); + + checkSimple(flags).toEqual(functionFlags.get(name)); + } + + if (libCode) { + checkSimple(lib["library_code"]).toEqual(libCode); + } + + break; + } + } + + expect(hasLib).toBeTruthy(); +} + /** * Check transaction response. * @param response - Transaction result received from `exec` call. @@ -913,6 +970,8 @@ export async function transactionTest( responseData.push(["functionLoad(code)", libName]); baseTransaction.functionLoad(code, true); responseData.push(["functionLoad(code, true)", libName]); + baseTransaction.functionList({ libNamePattern: "another" }); + responseData.push(['functionList("another")', []]); baseTransaction.fcall(funcName, [], ["one", "two"]); responseData.push(['fcall(funcName, [], ["one", "two"])', "one"]); baseTransaction.fcallReadonly(funcName, [], ["one", "two"]); @@ -928,6 +987,11 @@ export async function transactionTest( responseData.push(["functionFlush(FlushMode.ASYNC)", "OK"]); baseTransaction.functionFlush(FlushMode.SYNC); responseData.push(["functionFlush(FlushMode.SYNC)", "OK"]); + baseTransaction.functionList({ + libNamePattern: libName, + withCode: true, + }); + responseData.push(["functionList({ libName, true})", []]); } return responseData; From 1e3a396fa69740472b25b313f9bcc52151a3db27 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 26 Jul 2024 18:53:45 -0700 Subject: [PATCH 070/236] Update docs for `GEOSEARCH` command (#2017) * Update docs. Signed-off-by: Yury-Fridlyand Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> --- CHANGELOG.md | 1 + .../commands/geospatial/GeoSearchOptions.java | 10 ++-- .../commands/geospatial/GeoSearchOrigin.java | 20 +------- .../geospatial/GeoSearchResultOptions.java | 34 +++++++------ .../commands/geospatial/GeoSearchShape.java | 39 ++++----------- .../geospatial/GeoSearchStoreOptions.java | 11 ++--- .../models/commands/geospatial/GeoUnit.java | 6 +-- .../test/java/glide/SharedCommandTests.java | 48 +++++++++---------- .../java/glide/TransactionTestUtilities.java | 7 ++- python/python/glide/async_commands/core.py | 2 +- 10 files changed, 64 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ee3a7d36..ada796067e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Java, Python: Update docs for GEOSEARCH command ([#2017](https://github.com/valkey-io/valkey-glide/pull/2017)) * Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019)) * Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007)) * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java index 24cc1be42a..38dbaf399f 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java @@ -11,11 +11,11 @@ * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchOptions)} command, options include: * *
    - *
  • WITHDIST: Also return the distance of the returned items from the specified center point. - * The distance is returned in the same unit as specified for the searchBy - * argument. - *
  • WITHCOORD: Also return the coordinate of the returned items. - *
  • WITHHASH: Also return the geohash of the returned items. for the general user. + *
  • WITHDIST: Also return the distance of the returned items from the specified + * center point. The distance is returned in the same unit as specified for the searchBy + * argument. + *
  • WITHCOORD: Also return the coordinate of the returned items. + *
  • WITHHASH: Also return the geohash of the returned items. *
* * @see valkey.io diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java index 9be1aa333a..0caebd9013 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java @@ -15,7 +15,7 @@ public final class GeoSearchOrigin { /** Valkey API keyword used to perform search from the position of a given member. */ public static final String FROMMEMBER_VALKEY_API = "FROMMEMBER"; - /** Valkey API keyword used to perform search from the given longtitude & latitue position. */ + /** Valkey API keyword used to perform search from the given longitude & latitude position. */ public static final String FROMLONLAT_VALKEY_API = "FROMLONLAT"; /** @@ -36,12 +36,6 @@ public interface SearchOrigin { public static class CoordOrigin implements SearchOrigin { private final GeospatialData position; - /** - * Converts GeoSearchOrigin into a String[]. - * - * @return String[] An array containing arguments corresponding to the starting point of the - * query. - */ public String[] toArgs() { return ArrayUtils.addAll(new String[] {FROMLONLAT_VALKEY_API}, position.toArgs()); } @@ -52,12 +46,6 @@ public String[] toArgs() { public static class MemberOrigin implements SearchOrigin { private final String member; - /** - * Converts GeoSearchOrigin into a String[]. - * - * @return String[] An array containing arguments corresponding to the starting point of the - * query. - */ public String[] toArgs() { return new String[] {FROMMEMBER_VALKEY_API, member}; } @@ -68,12 +56,6 @@ public String[] toArgs() { public static class MemberOriginBinary implements SearchOrigin { private final GlideString member; - /** - * Converts GeoSearchOrigin into a String[]. - * - * @return String[] An array containing arguments corresponding to the starting point of the - * query. - */ public String[] toArgs() { return new String[] {FROMMEMBER_VALKEY_API, member.toString()}; } diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java index 5a4e6c8b29..a7aa7cc4bd 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java @@ -8,20 +8,16 @@ /** * Optional arguments for {@link GeospatialIndicesBaseCommands#geosearch(String, - * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchOptions)} command that contains up to 2 - * optional input, including: + * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchOptions)} command that contains up to 3 + * optional inputs, including: * *
    - *
  • SortOrder: The query's order to sort the results by: - *
      - *
    • ASC: Sort returned items from the nearest to the farthest, relative to the center - * point. - *
    • DESC: Sort returned items from the farthest to the nearest, relative to the center - * point. - *
    - *
  • COUNT: Limits the results to the first N matching items, when the 'ANY' option is used, the - * command returns as soon as enough matches are found. This means that the results might may - * not be the ones closest to the origin. + *
  • {@link SortOrder} to order the search results by the distance to the center point of the + * search area. + *
  • COUNT to limit the number of search results. + *
  • ANY, which can only be used if COUNT is also provided. This + * option makes the command return as soon as enough matches are found. This means that the + * results might not be the ones closest to the origin. *
* * @see valkey.io @@ -42,38 +38,40 @@ public class GeoSearchResultOptions { /** Indicates the number of matches the result should be limited to. */ private final long count; - /** Indicates if the 'ANY' keyword is used for 'COUNT'. */ + /** Whether to allow returning as soon as enough matches are found. */ private final boolean isAny; - /** Constructor with count only. */ + /** Define number of search results. */ public GeoSearchResultOptions(long count) { this.sortOrder = null; this.count = count; this.isAny = false; } - /** Constructor with count and the 'ANY' keyword. */ + /** + * Define number of search results, and whether or not the ANY option should be used. + */ public GeoSearchResultOptions(long count, boolean isAny) { this.sortOrder = null; this.count = count; this.isAny = isAny; } - /** Constructor with sort order only. */ + /** Define the sort order only. */ public GeoSearchResultOptions(SortOrder order) { this.sortOrder = order; this.count = -1; this.isAny = false; } - /** Constructor with sort order and count. */ + /** Define the sort order and count. */ public GeoSearchResultOptions(SortOrder order, long count) { this.sortOrder = order; this.count = count; this.isAny = false; } - /** Constructor with sort order, count and 'ANY' keyword. */ + /** Configure all parameters. */ public GeoSearchResultOptions(SortOrder order, long count, boolean isAny) { this.sortOrder = order; this.count = count; diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java index 3add2dadd9..75ffe7faf3 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java @@ -2,54 +2,33 @@ package glide.api.models.commands.geospatial; import glide.api.commands.GeospatialIndicesBaseCommands; -import lombok.Getter; /** * The query's shape for {@link GeospatialIndicesBaseCommands} command. * * @see valkey.io */ -@Getter public final class GeoSearchShape { - /** Valkey API keyword used to perform geosearch by radius. */ - public static final String BYRADIUS_VALKEY_API = "BYRADIUS"; - /** Valkey API keyword used to perform geosearch by box. */ - public static final String BYBOX_VALKEY_API = "BYBOX"; - - /** - * The geosearch query's shape: - * - *
    - *
  • BYRADIUS - Circular shaped query. - *
  • BYBOX - Box shaped query. - *
- */ - public enum SearchShape { + /** The geosearch query's shape. */ + enum SearchShape { + /** Circular shaped query. */ BYRADIUS, + /** Rectangular shaped query. */ BYBOX } - /** The geosearch query's shape. */ private final SearchShape shape; - - /** The circular geosearch query's radius. */ private final double radius; - - /** The box geosearch query's width. */ private final double width; - - /** The box geosearch query's height. */ private final double height; - - /** The geosearch query's metric unit. */ private final GeoUnit unit; /** - * BYRADIUS constructor for GeoSearchShape + * Defines a circular search area. * * @param radius The radius to search by. - * @param unit The unit of the radius. + * @param unit The measurement unit of the radius. */ public GeoSearchShape(double radius, GeoUnit unit) { this.shape = SearchShape.BYRADIUS; @@ -62,11 +41,11 @@ public GeoSearchShape(double radius, GeoUnit unit) { } /** - * BYBOX constructor for GeoSearchShape + * Defines a rectangular search area. * * @param width The width to search by. * @param height The height to search by. - * @param unit The unit of the radius. + * @param unit The measurement unit of the width and height. */ public GeoSearchShape(double width, double height, GeoUnit unit) { this.shape = SearchShape.BYBOX; @@ -91,7 +70,7 @@ public String[] toArgs() { return new String[] { shape.toString(), Double.toString(width), Double.toString(height), unit.getValkeyAPI() }; - default: + default: // unreachable return new String[] {}; } } diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java index ec2c50d34e..047273aaa3 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java @@ -12,16 +12,10 @@ */ @Builder public final class GeoSearchStoreOptions { - /** - * Valkey API keyword used to perform geosearchstore and optionally sort the results with their - * distance from the center. - */ + /** Valkey API keyword for {@link #storeDist} parameter. */ public static final String GEOSEARCHSTORE_VALKEY_API = "STOREDIST"; - /** - * boolean value indicating if the STOREDIST option should be included. Can be included in builder - * construction by using {@link GeoSearchStoreOptionsBuilder#storedist()}. - */ + /** Configure sorting the results with their distance from the center. */ private final boolean storeDist; /** @@ -40,6 +34,7 @@ public String[] toArgs() { public static class GeoSearchStoreOptionsBuilder { public GeoSearchStoreOptionsBuilder() {} + /** Enable sorting the results with their distance from the center. */ public GeoSearchStoreOptionsBuilder storedist() { return storeDist(true); } diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java index b81bcecdeb..913796f060 100644 --- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java @@ -1,14 +1,10 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands.geospatial; -import glide.api.commands.GeospatialIndicesBaseCommands; import lombok.Getter; import lombok.RequiredArgsConstructor; -/** - * Enumeration representing distance units options for the {@link - * GeospatialIndicesBaseCommands#geodist(String, String, String, GeoUnit)} command. - */ +/** Enumeration representing distance units options for the geospatial command. */ @RequiredArgsConstructor @Getter public enum GeoUnit { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 9832ec2e7c..f4256bf4fb 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -12594,8 +12594,8 @@ public void geosearch(BaseClient client) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String[] members = {"Catania", "Palermo", "edge2", "edge1"}; - Set members_set = Set.of(members); - GeospatialData[] members_coordinates = { + Set membersSet = Set.of(members); + GeospatialData[] membersCoordinates = { new GeospatialData(15.087269, 37.502669), new GeospatialData(13.361389, 38.115556), new GeospatialData(17.241510, 38.788135), @@ -12636,18 +12636,18 @@ public void geosearch(BaseClient client) { key1, Map.of( members[0], - members_coordinates[0], + membersCoordinates[0], members[1], - members_coordinates[1], + membersCoordinates[1], members[2], - members_coordinates[2], + membersCoordinates[2], members[3], - members_coordinates[3])) + membersCoordinates[3])) .get()); // Search by box, unit: km, from a geospatial data point assertTrue( - members_set.containsAll( + membersSet.containsAll( Set.of( client .geosearch( @@ -12732,26 +12732,26 @@ public void geosearch(BaseClient client) { .get()[0]); // test search by radius, units: feet, from member - double feet_radius = 200 * 3280.8399; + double feetRadius = 200 * 3280.8399; assertArrayEquals( new String[] {"Catania", "Palermo"}, client .geosearch( key1, new MemberOrigin("Catania"), - new GeoSearchShape(feet_radius, GeoUnit.FEET), + new GeoSearchShape(feetRadius, GeoUnit.FEET), new GeoSearchResultOptions(SortOrder.ASC)) .get()); // Test search by radius, unit: meters, from member - double meters_radius = 200 * 1000; + double metersRadius = 200 * 1000; assertArrayEquals( new String[] {"Palermo", "Catania"}, client .geosearch( key1, new MemberOrigin("Catania"), - new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchShape(metersRadius, GeoUnit.METERS), new GeoSearchResultOptions(SortOrder.DESC)) .get()); assertDeepEquals( @@ -12763,7 +12763,7 @@ public void geosearch(BaseClient client) { .geosearch( key1, new MemberOriginBinary(gs("Catania")), - new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchShape(metersRadius, GeoUnit.METERS), GeoSearchOptions.builder().withhash().build()) .get()); @@ -12875,8 +12875,8 @@ public void geosearch_binary(BaseClient client) { GlideString key1 = gs("{key}-1" + UUID.randomUUID()); GlideString key2 = gs("{key}-2" + UUID.randomUUID()); GlideString[] members = {gs("Catania"), gs("Palermo"), gs("edge2"), gs("edge1")}; - Set members_set = Set.of(members); - GeospatialData[] members_coordinates = { + Set membersSet = Set.of(members); + GeospatialData[] membersCoordinates = { new GeospatialData(15.087269, 37.502669), new GeospatialData(13.361389, 38.115556), new GeospatialData(17.241510, 38.788135), @@ -12917,18 +12917,18 @@ public void geosearch_binary(BaseClient client) { key1, Map.of( members[0], - members_coordinates[0], + membersCoordinates[0], members[1], - members_coordinates[1], + membersCoordinates[1], members[2], - members_coordinates[2], + membersCoordinates[2], members[3], - members_coordinates[3])) + membersCoordinates[3])) .get()); // Search by box, unit: km, from a geospatial data point assertTrue( - members_set.containsAll( + membersSet.containsAll( Set.of( client .geosearch( @@ -13013,26 +13013,26 @@ public void geosearch_binary(BaseClient client) { .get()[0]); // test search by radius, units: feet, from member - double feet_radius = 200 * 3280.8399; + double feetRadius = 200 * 3280.8399; assertArrayEquals( new GlideString[] {gs("Catania"), gs("Palermo")}, client .geosearch( key1, new MemberOriginBinary(gs("Catania")), - new GeoSearchShape(feet_radius, GeoUnit.FEET), + new GeoSearchShape(feetRadius, GeoUnit.FEET), new GeoSearchResultOptions(SortOrder.ASC)) .get()); // Test search by radius, unit: meters, from member - double meters_radius = 200 * 1000; + double metersRadius = 200 * 1000; assertArrayEquals( new GlideString[] {gs("Palermo"), gs("Catania")}, client .geosearch( key1, new MemberOriginBinary(gs("Catania")), - new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchShape(metersRadius, GeoUnit.METERS), new GeoSearchResultOptions(SortOrder.DESC)) .get()); @@ -13045,7 +13045,7 @@ public void geosearch_binary(BaseClient client) { .geosearch( key1, new MemberOriginBinary(gs("Catania")), - new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchShape(metersRadius, GeoUnit.METERS), GeoSearchOptions.builder().withhash().build()) .get()); // Test search by radius, unit: miles, from geospatial data diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 96b0c9f894..c874dbb3b1 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -1085,7 +1085,7 @@ private static Object[] geospatialCommands(BaseTransaction transaction) { }, // geosearch(geoKey1, "Palermo", byradius(200, km)) new String[] { "Palermo", "Catania" - }, // geosearch(geoKey1, (15,37), bybox(200,200,km)) + }, // geosearch(geoKey1, (15, 37), bybox(400, 400, km)) new Object[] { new Object[] { "Palermo", @@ -1120,9 +1120,8 @@ private static Object[] geospatialCommands(BaseTransaction transaction) { } }, }, // geosearch(geoKey1, (15,37), BYBOX(400,400,km), ASC, COUNT 2) - 2L, // geosearch(geoKey2, geoKey1, (15,37), BYBOX(400,400,km), ASC, COUNT 2) - 2L, // geosearch(geoKey2, geoKey1, (15,37), BYBOX(400,400,km), STOREDIST, ASC, COUNT - // 2) + 2L, // geosearchstore(geoKey2, geoKey1, (15, 37), (400, 400, km), ASC, 2) + 2L, // geosearchstore(geoKey2, geoKey1, (15, 37), (400, 400, km), STOREDIST, ASC, 2) }); } diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index a267c73d25..35d02a7eff 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -3861,7 +3861,7 @@ async def geosearch( from the sorted set or as a geospatial data (see `GeospatialData`). search_by (Union[GeoSearchByRadius, GeoSearchByBox]): The search criteria. For circular area search, see `GeoSearchByRadius`. - For rectengal area search, see `GeoSearchByBox`. + For rectangular area search, see `GeoSearchByBox`. order_by (Optional[OrderBy]): Specifies the order in which the results should be returned. - `ASC`: Sorts items from the nearest to the farthest, relative to the center point. - `DESC`: Sorts items from the farthest to the nearest, relative to the center point. From f321e5be4798f56ea1efda9010fb294ea4772ad9 Mon Sep 17 00:00:00 2001 From: barshaul Date: Sat, 27 Jul 2024 12:44:22 +0000 Subject: [PATCH 071/236] Fixes for rust 1.80.0 Signed-off-by: barshaul --- glide-core/Cargo.toml | 1 + glide-core/src/client/standalone_client.rs | 8 ++++---- glide-core/tests/test_standalone_client.rs | 9 ++++----- logger_core/Cargo.toml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/glide-core/Cargo.toml b/glide-core/Cargo.toml index b03196c8f3..d64009851d 100644 --- a/glide-core/Cargo.toml +++ b/glide-core/Cargo.toml @@ -30,6 +30,7 @@ nanoid = "0.4.0" [features] socket-layer = ["directories", "integer-encoding", "num_cpus", "protobuf", "tokio-util"] +standalone_heartbeat = [] [dev-dependencies] rsevents = "0.3.1" diff --git a/glide-core/src/client/standalone_client.rs b/glide-core/src/client/standalone_client.rs index 727ad906d9..736b13d312 100644 --- a/glide-core/src/client/standalone_client.rs +++ b/glide-core/src/client/standalone_client.rs @@ -6,7 +6,7 @@ use super::reconnecting_connection::ReconnectingConnection; use super::{ConnectionRequest, NodeAddress, TlsMode}; use crate::retry_strategies::RetryStrategy; use futures::{future, stream, StreamExt}; -#[cfg(standalone_heartbeat)] +#[cfg(feature = "standalone_heartbeat")] use logger_core::log_debug; use logger_core::log_warn; use rand::Rng; @@ -15,7 +15,7 @@ use redis::{PushInfo, RedisError, RedisResult, Value}; use std::sync::atomic::AtomicUsize; use std::sync::Arc; use tokio::sync::mpsc; -#[cfg(standalone_heartbeat)] +#[cfg(feature = "standalone_heartbeat")] use tokio::task; #[derive(Debug)] @@ -185,7 +185,7 @@ impl StandaloneClient { } let read_from = get_read_from(connection_request.read_from); - #[cfg(standalone_heartbeat)] + #[cfg(feature = "standalone_heartbeat")] for node in nodes.iter() { Self::start_heartbeat(node.clone()); } @@ -363,7 +363,7 @@ impl StandaloneClient { } } - #[cfg(standalone_heartbeat)] + #[cfg(feature = "standalone_heartbeat")] fn start_heartbeat(reconnecting_connection: ReconnectingConnection) { task::spawn(async move { loop { diff --git a/glide-core/tests/test_standalone_client.rs b/glide-core/tests/test_standalone_client.rs index 75e3262f80..28d09f8b81 100644 --- a/glide-core/tests/test_standalone_client.rs +++ b/glide-core/tests/test_standalone_client.rs @@ -5,9 +5,8 @@ mod utilities; #[cfg(test)] mod standalone_client_tests { - use std::collections::HashMap; - use crate::utilities::mocks::{Mock, ServerMock}; + use std::collections::HashMap; use super::*; use glide_core::{ @@ -59,12 +58,12 @@ mod standalone_client_tests { #[rstest] #[serial_test::serial] #[timeout(LONG_STANDALONE_TEST_TIMEOUT)] - #[cfg(standalone_heartbeat)] + #[cfg(feature = "standalone_heartbeat")] fn test_detect_disconnect_and_reconnect_using_heartbeat(#[values(false, true)] use_tls: bool) { let (sender, receiver) = tokio::sync::oneshot::channel(); block_on_all(async move { let mut test_basics = setup_test_basics(use_tls).await; - let server = test_basics.server; + let server = test_basics.server.expect("Server shouldn't be None"); let address = server.get_client_addr(); drop(server); @@ -79,7 +78,7 @@ mod standalone_client_tests { let _new_server = receiver.await; tokio::time::sleep( - glide_core::client::HEARTBEAT_SLEEP_DURATION + Duration::from_secs(1), + glide_core::client::HEARTBEAT_SLEEP_DURATION + std::time::Duration::from_secs(1), ) .await; diff --git a/logger_core/Cargo.toml b/logger_core/Cargo.toml index be306d4751..f2b7512e14 100644 --- a/logger_core/Cargo.toml +++ b/logger_core/Cargo.toml @@ -13,7 +13,7 @@ test-env-helpers = "0.2.2" [dependencies] tracing = "0.1" -tracing-appender = "0.2.2" +tracing-appender = { version = "0.2.3", default-features = false } once_cell = "1.16.0" file-rotate = "0.7.1" tracing-subscriber = "0.3.17" From d89a9deffc2c9cc8a8212137aa7b0eadd0bb2351 Mon Sep 17 00:00:00 2001 From: barshaul Date: Sat, 27 Jul 2024 12:44:34 +0000 Subject: [PATCH 072/236] Merge from upstream redis-rs Signed-off-by: barshaul --- submodules/redis-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/redis-rs b/submodules/redis-rs index ee3119d17d..b8c921d718 160000 --- a/submodules/redis-rs +++ b/submodules/redis-rs @@ -1 +1 @@ -Subproject commit ee3119d17dfafc57aba154be991efaccbe2833f9 +Subproject commit b8c921d7186855997a8ff89465653e84bfa4eb38 From a302dc83c6e2cee358e66a6c4dc1f744f3e50772 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:17:36 +0300 Subject: [PATCH 073/236] submodules: merge from redis-rs for pubsub (#2044) Signed-off-by: Shoham Elias --- glide-core/src/client/standalone_client.rs | 3 +++ submodules/redis-rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/glide-core/src/client/standalone_client.rs b/glide-core/src/client/standalone_client.rs index 736b13d312..a8350651e9 100644 --- a/glide-core/src/client/standalone_client.rs +++ b/glide-core/src/client/standalone_client.rs @@ -309,6 +309,9 @@ impl StandaloneClient { Some(ResponsePolicy::CombineArrays) => future::try_join_all(requests) .await .and_then(cluster_routing::combine_array_results), + Some(ResponsePolicy::CombineMaps) => future::try_join_all(requests) + .await + .and_then(cluster_routing::combine_map_results), Some(ResponsePolicy::Special) | None => { // This is our assumption - if there's no coherent way to aggregate the responses, we just collect them in an array, and pass it to the user. // TODO - once Value::Error is merged, we can use join_all and report separate errors and also pass successes. diff --git a/submodules/redis-rs b/submodules/redis-rs index b8c921d718..de53b2b5c6 160000 --- a/submodules/redis-rs +++ b/submodules/redis-rs @@ -1 +1 @@ -Subproject commit b8c921d7186855997a8ff89465653e84bfa4eb38 +Subproject commit de53b2b5c68e7ef4667e2b195eb9b1d0dd460722 From d69310502ce7e1e62021aae25240319b6901ef54 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 29 Jul 2024 10:11:35 -0700 Subject: [PATCH 074/236] Node client CI: Add reporting (#1983) Signed-off-by: Yury-Fridlyand --- .github/workflows/node.yml | 120 +++++++++++++++++++++++++----------- node/.gitignore | 2 + node/jest.config.js | 12 ++++ node/package.json | 1 + node/tests/TestUtilities.ts | 7 +-- 5 files changed, 100 insertions(+), 42 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index b3253bd17d..32db45e5c5 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -59,7 +59,7 @@ jobs: fail-fast: false matrix: engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} - + steps: - uses: actions/checkout@v4 with: @@ -85,21 +85,36 @@ jobs: - name: test hybrid node modules - commonjs run: | npm install --package-lock-only - npm ci + npm ci npm run build-and-test working-directory: ./node/hybrid-node-tests/commonjs-test - + env: + JEST_HTML_REPORTER_OUTPUT_PATH: test-report-commonjs.html + - name: test hybrid node modules - ecma run: | npm install --package-lock-only npm ci npm run build-and-test working-directory: ./node/hybrid-node-tests/ecmascript-test + env: + JEST_HTML_REPORTER_OUTPUT_PATH: test-report-ecma.html - uses: ./.github/workflows/test-benchmark with: language-flag: -node + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-report-node-${{ matrix.engine.type }}-${{ matrix.engine.version }}-ubuntu + path: | + node/test-report*.html + utils/clusters/** + benchmarks/results/** + lint-rust: timeout-minutes: 15 runs-on: ubuntu-latest @@ -147,6 +162,17 @@ jobs: # run: npm test -- -t "set and get flow works" # working-directory: ./node + # - name: Upload test reports + # if: always() + # continue-on-error: true + # uses: actions/upload-artifact@v4 + # with: + # name: test-report-node-${{ matrix.engine.type }}-${{ matrix.engine.version }}-macos + # path: | + # node/test-report*.html + # utils/clusters/** + # benchmarks/results/** + build-amazonlinux-latest: runs-on: ubuntu-latest container: amazonlinux:latest @@ -157,7 +183,7 @@ jobs: yum -y remove git yum -y remove git-* yum -y install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm - yum install -y git + yum install -y git git --version - uses: actions/checkout@v4 @@ -181,8 +207,19 @@ jobs: - name: Test compatibility run: npm test -- -t "set and get flow works" - working-directory: ./node - + working-directory: ./node + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-report-node-amazonlinux + path: | + node/test-report*.html + utils/clusters/** + benchmarks/results/** + build-and-test-linux-musl-on-x86: name: Build and test Node wrapper on Linux musl runs-on: ubuntu-latest @@ -191,33 +228,44 @@ jobs: options: --user root --privileged steps: - - name: Install git - run: | - apk update - apk add git - - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Setup musl on Linux - uses: ./.github/workflows/setup-musl-on-linux - with: - workspace: $GITHUB_WORKSPACE - npm-scope: ${{ secrets.NPM_SCOPE }} - npm-auth-token: ${{ secrets.NPM_AUTH_TOKEN }} - - - name: Build Node wrapper - uses: ./.github/workflows/build-node-wrapper - with: - os: ubuntu - named_os: linux - arch: x64 - target: x86_64-unknown-linux-musl - github-token: ${{ secrets.GITHUB_TOKEN }} - engine-version: "7.2.5" - - - name: Test compatibility - shell: bash - run: npm test -- -t "set and get flow works" - working-directory: ./node + - name: Install git + run: | + apk update + apk add git + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup musl on Linux + uses: ./.github/workflows/setup-musl-on-linux + with: + workspace: $GITHUB_WORKSPACE + npm-scope: ${{ secrets.NPM_SCOPE }} + npm-auth-token: ${{ secrets.NPM_AUTH_TOKEN }} + + - name: Build Node wrapper + uses: ./.github/workflows/build-node-wrapper + with: + os: ubuntu + named_os: linux + arch: x64 + target: x86_64-unknown-linux-musl + github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: "7.2.5" + + - name: Test compatibility + shell: bash + run: npm test -- -t "set and get flow works" + working-directory: ./node + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-report-node-linux-musl + path: | + node/test-report*.html + utils/clusters/** + benchmarks/results/** diff --git a/node/.gitignore b/node/.gitignore index 1cae12ee73..212788384f 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -8,6 +8,8 @@ lerna-debug.log* .pnpm-debug.log* rust-client/index.* +# Test reports +*.html # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/node/jest.config.js b/node/jest.config.js index f58b30d383..f47356fab9 100644 --- a/node/jest.config.js +++ b/node/jest.config.js @@ -14,4 +14,16 @@ module.exports = { "mjs", ], testTimeout: 20000, + reporters: [ + "default", + [ + "./node_modules/jest-html-reporter", + { + includeFailureMsg: true, + includeSuiteFailure: true, + executionTimeWarningThreshold: 60, + sort: "status", + }, + ], + ], }; diff --git a/node/package.json b/node/package.json index 16dd9851b4..2f7a2e180b 100644 --- a/node/package.json +++ b/node/package.json @@ -55,6 +55,7 @@ "eslint-plugin-tsdoc": "^0.2.17", "find-free-port": "^2.0.0", "jest": "^28.1.3", + "jest-html-reporter": "^3.10.2", "protobufjs-cli": "^1.1.1", "redis-server": "^1.2.2", "replace": "^1.2.2", diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 3f0b25e000..479c9da083 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -2,7 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { beforeAll, expect } from "@jest/globals"; +import { expect } from "@jest/globals"; import { exec } from "child_process"; import parseArgs from "minimist"; import { v4 as uuidv4 } from "uuid"; @@ -20,7 +20,6 @@ import { GlideClient, GlideClusterClient, InsertPosition, - Logger, ListDirection, ProtocolVersion, ReturnType, @@ -29,10 +28,6 @@ import { Transaction, } from ".."; -beforeAll(() => { - Logger.init("info"); -}); - /* eslint-disable @typescript-eslint/no-explicit-any */ function intoArrayInternal(obj: any, builder: Array) { if (obj == null) { From f69904e59ef426ed13238d268e4150f709add9f5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 29 Jul 2024 10:12:19 -0700 Subject: [PATCH 075/236] Node: Exported client configuration types (#2023) * Fix exports. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 6 ++++++ node/src/BaseClient.ts | 11 ++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada796067e..2e0370e1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023)) * Java, Python: Update docs for GEOSEARCH command ([#2017](https://github.com/valkey-io/valkey-glide/pull/2017)) * Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019)) * Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index f9f35d8685..b22818940d 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -113,9 +113,12 @@ function initialize() { ZaddOptions, ScoreBoundry, UpdateOptions, + ProtocolVersion, RangeByIndex, RangeByScore, RangeByLex, + ReadFrom, + RedisCredentials, SortedSetRange, StreamTrimOptions, StreamAddOptions, @@ -181,9 +184,12 @@ function initialize() { ZaddOptions, ScoreBoundry, UpdateOptions, + ProtocolVersion, RangeByIndex, RangeByScore, RangeByLex, + ReadFrom, + RedisCredentials, SortedSetRange, StreamTrimOptions, StreamAddOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index bdf192a33a..3d0a6c284c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -198,7 +198,8 @@ export type ReturnType = | ReturnTypeAttribute | ReturnType[]; -type RedisCredentials = { +/** Represents the credentials for connecting to a server. */ +export type RedisCredentials = { /** * The username that will be used for authenticating connections to the Redis servers. * If not supplied, "default" will be used. @@ -210,13 +211,17 @@ type RedisCredentials = { password: string; }; -type ReadFrom = +/** Represents the client's read from strategy. */ +export type ReadFrom = /** Always get from primary, in order to get the freshest data.*/ | "primary" /** Spread the requests between all replicas in a round robin manner. If no replica is available, route the requests to the primary.*/ | "preferReplica"; +/** + * Configuration settings for creating a client. Shared settings for standalone and cluster clients. + */ export type BaseClientConfiguration = { /** * DNS Addresses and ports of known nodes in the cluster. @@ -3954,7 +3959,7 @@ export class BaseClient { ): connection_request.IConnectionRequest { const readFrom = options.readFrom ? this.MAP_READ_FROM_STRATEGY[options.readFrom] - : undefined; + : connection_request.ReadFrom.Primary; const authenticationInfo = options.credentials !== undefined && "password" in options.credentials From 8926856f472752e913118d59cbacfbd54085f2c7 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 11:33:23 -0700 Subject: [PATCH 076/236] address comments and fixed tests Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 6 +++--- node/src/Transaction.ts | 4 ++-- node/tests/RedisClusterClient.test.ts | 8 ++++++++ node/tests/SharedTests.ts | 23 +++-------------------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9d463c0282..e2cda7b055 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1594,9 +1594,9 @@ export class BaseClient { * Blocks the connection until it pops atomically and removes the left/right-most element to the * list stored at `source` depending on `whereFrom`, and pushes the element at the first/last element * of the list stored at `destination` depending on `whereTo`. - * `BLMOVE` is the blocking variant of `LMOVE`. + * `BLMOVE` is the blocking variant of {@link lmove}. * - * Note: + * @remarks * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. * @@ -1622,7 +1622,7 @@ export class BaseClient { * console.log(result2); // Output: "two" * * const updated_array2 = await client.lrange("testKey2", 0, -1); - * console.log(updated_array2); // Output: "one", "three", "four"] + * console.log(updated_array2); // Output: ["one", "three", "four"] * ``` */ public async blmove( diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 62a6a55f56..1f1ae10f37 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -799,9 +799,9 @@ export class BaseTransaction> { * Blocks the connection until it pops atomically and removes the left/right-most element to the * list stored at `source` depending on `whereFrom`, and pushes the element at the first/last element * of the list stored at `destination` depending on `whereTo`. - * `BLMOVE` is the blocking variant of `LMOVE`. + * `BLMOVE` is the blocking variant of {@link lmove}. * - * Note: + * @remarks * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index b60f064b69..4f63e58da2 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -17,6 +17,7 @@ import { ClusterTransaction, GlideClusterClient, InfoOptions, + ListDirection, ProtocolVersion, Routes, ScoreFilter, @@ -322,6 +323,13 @@ describe("GlideClusterClient", () => { if (gte(cluster.getVersion(), "6.2.0")) { promises.push( + client.blmove( + "abc", + "def", + ListDirection.LEFT, + ListDirection.LEFT, + 0.2, + ), client.zdiff(["abc", "zxy", "lkn"]), client.zdiffWithScores(["abc", "zxy", "lkn"]), client.zdiffstore("abc", ["zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 828988ef7e..1efd5f6bb7 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,6 +29,7 @@ import { Script, UpdateByScore, parseInfoResponse, + TimeoutError, } from "../"; import { RedisCluster } from "../../utils/TestUtils"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; @@ -1454,26 +1455,8 @@ export function runBaseTests(config: { ), ).rejects.toThrow(RequestError); - // BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to - // avoid having the test block forever - async function blmove_timeout_test() { - await wait(50000); - await expect( - client.blmove( - "{SameSlot}non_existing_key", - key2, - ListDirection.LEFT, - ListDirection.RIGHT, - 0, - ), - ).rejects.toThrow(ClosingError); - } - - try { - blmove_timeout_test(); - } catch (ClosingError) { - console.log("Closing error with timeout occurred."); - } + // TODO: add test case with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever }, protocol); }, config.timeout, From bbd62d561971dede232255fcda55896b1612aec2 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 11:43:19 -0700 Subject: [PATCH 077/236] changed since Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 4 ++-- node/src/Transaction.ts | 2 +- node/tests/SharedTests.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 48fd9ce62f..f7e2d38720 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1643,8 +1643,8 @@ export class BaseClient { * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. * - * Since: Valkey version 6.2.0. - * + * since Valkey version 6.2.0. + * * @example * ```typescript * await client.lpush("testKey1", ["two", "one"]); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index eeb396eee2..b6a5eca3c2 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -841,7 +841,7 @@ export class BaseTransaction> { * * Command Response - The popped element, or `null` if `source` does not exist or if the operation timed-out. * - * Since: Valkey version 6.2.0. + * since Valkey version 6.2.0. */ public blmove( source: string, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 268d261d09..ee4a2453eb 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -30,7 +30,6 @@ import { SortOrder, UpdateByScore, parseInfoResponse, - TimeoutError, } from "../"; import { RedisCluster } from "../../utils/TestUtils"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; From 9adbfcbf132e9f0f06d3f76a33c5b79d59b1ffe2 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 11:44:44 -0700 Subject: [PATCH 078/236] ran linter 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 f7e2d38720..dc17a5eec1 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1644,7 +1644,7 @@ export class BaseClient { * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. * * since Valkey version 6.2.0. - * + * * @example * ```typescript * await client.lpush("testKey1", ["two", "one"]); From e3dd23d862109b9e57bd793e5413bd6e8d3b68fd Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:10:46 -0700 Subject: [PATCH 079/236] Add command ZRandMember (#2013) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 91 +++++++++++++++++++++++- node/src/Commands.ts | 21 ++++++ node/src/Transaction.ts | 47 +++++++++++++ node/tests/SharedTests.ts | 133 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 12 ++++ 6 files changed, 304 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0370e1b3..3271ffb9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,7 @@ * Node: Added GeoDist command ([#1988](https://github.com/valkey-io/valkey-glide/pull/1988)) * Node: Added GeoHash command ([#1997](https://github.com/valkey-io/valkey-glide/pull/1997)) * Node: Added HStrlen command ([#2020](https://github.com/valkey-io/valkey-glide/pull/2020)) +* Node: Added ZRandMember command ([#2013](https://github.com/valkey-io/valkey-glide/pull/2013)) #### 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/BaseClient.ts b/node/src/BaseClient.ts index 3d0a6c284c..78f7fc92b9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1,7 +1,6 @@ /** * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ - import { DEFAULT_TIMEOUT_IN_MILLISECONDS, Script, @@ -144,6 +143,7 @@ import { createZMScore, createZPopMax, createZPopMin, + createZRandMember, createZRange, createZRangeWithScores, createZRank, @@ -193,6 +193,7 @@ export type ReturnType = | null | boolean | bigint + | Buffer | Set | ReturnTypeMap | ReturnTypeAttribute @@ -2840,6 +2841,94 @@ export class BaseClient { ); } + /** + * Returns a random member from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @returns A string representing a random member from the sorted set. + * If the sorted set does not exist or is empty, the response will be `null`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmember("mySortedSet"); + * console.log(payload1); // Output: "Glide" (a random member from the set) + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmember("nonExistingSortedSet"); + * console.log(payload2); // Output: null since the sorted set does not exist. + * ``` + */ + public async zrandmember(key: string): Promise { + return this.createWritePromise(createZRandMember(key)); + } + + /** + * Returns random members from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * @returns An `array` of members from the sorted set. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmemberWithCount("mySortedSet", -3); + * console.log(payload1); // Output: ["Glide", "GLIDE", "node"] + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmemberWithCount("nonExistingKey", 3); + * console.log(payload1); // Output: [] since the sorted set does not exist. + * ``` + */ + public async zrandmemberWithCount( + key: string, + count: number, + ): Promise { + return this.createWritePromise(createZRandMember(key, count)); + } + + /** + * Returns random members with scores from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * @returns A 2D `array` of `[member, score]` `arrays`, where + * member is a `string` and score is a `number`. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmemberWithCountWithScore("mySortedSet", -3); + * console.log(payload1); // Output: [["Glide", 1.0], ["GLIDE", 1.0], ["node", 2.0]] + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmemberWithCountWithScore("nonExistingKey", 3); + * console.log(payload1); // Output: [] since the sorted set does not exist. + * ``` + */ + public async zrandmemberWithCountWithScores( + key: string, + count: number, + ): Promise<[string, number][]> { + return this.createWritePromise(createZRandMember(key, count, true)); + } + /** Returns the length of the string value stored at `key`. * See https://valkey.io/commands/strlen/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e8377c9e47..19f8ceed2e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2406,3 +2406,24 @@ export function createHStrlen( ): command_request.Command { return createCommand(RequestType.HStrlen, [key, field]); } + +/** + * @internal + */ +export function createZRandMember( + key: string, + count?: number, + withscores?: boolean, +): command_request.Command { + const args = [key]; + + if (count !== undefined) { + args.push(count.toString()); + } + + if (withscores) { + args.push("WITHSCORES"); + } + + return createCommand(RequestType.ZRandMember, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 24be54d0a6..073194bc20 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -160,6 +160,7 @@ import { createZMScore, createZPopMax, createZPopMin, + createZRandMember, createZRange, createZRangeWithScores, createZRank, @@ -1523,6 +1524,52 @@ export class BaseTransaction> { ); } + /** + * Returns a random member from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * Command Response - A string representing a random member from the sorted set. + * If the sorted set does not exist or is empty, the response will be `null`. + */ + public zrandmember(key: string): T { + return this.addAndReturn(createZRandMember(key)); + } + + /** + * Returns random members from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * Command Response - An `array` of members from the sorted set. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + */ + public zrandmemberWithCount(key: string, count: number): T { + return this.addAndReturn(createZRandMember(key, count)); + } + + /** + * Returns random members with scores from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * Command Response - A 2D `array` of `[member, score]` `arrays`, where + * member is a `string` and score is a `number`. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + */ + public zrandmemberWithCountWithScores(key: string, count: number): T { + return this.addAndReturn(createZRandMember(key, count, true)); + } + /** Returns the string representation of the type of the value stored at `key`. * See https://valkey.io/commands/type/ for more details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 203136507b..cb43d3ce51 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5446,6 +5446,139 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmember test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + const elements = ["one", "two"]; + expect(await client.zadd(key1, memberScores)).toBe(2); + + // check random memember belongs to the set + const randmember = await client.zrandmember(key1); + + if (randmember !== null) { + checkSimple(randmember in elements).toEqual(true); + } + + // non existing key should return null + expect(await client.zrandmember("nonExistingKey")).toBeNull(); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect(client.zrandmember(key2)).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmemberWithCount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + expect(await client.zadd(key1, memberScores)).toBe(2); + + // unique values are expected as count is positive + let randMembers = await client.zrandmemberWithCount(key1, 4); + expect(randMembers.length).toBe(2); + expect(randMembers.length).toEqual(new Set(randMembers).size); + + // Duplicate values are expected as count is negative + randMembers = await client.zrandmemberWithCount(key1, -4); + expect(randMembers.length).toBe(4); + const randMemberSet = new Set(); + + for (const member of randMembers) { + const memberStr = member + ""; + + if (!randMemberSet.has(memberStr)) { + randMemberSet.add(memberStr); + } + } + + expect(randMembers.length).not.toEqual(randMemberSet.size); + + // non existing key should return empty array + randMembers = await client.zrandmemberWithCount( + "nonExistingKey", + -4, + ); + expect(randMembers.length).toBe(0); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect( + client.zrandmemberWithCount(key2, 1), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmemberWithCountWithScores test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + const memberScoreMap = new Map([ + ["one", 1.0], + ["two", 2.0], + ]); + expect(await client.zadd(key1, memberScores)).toBe(2); + + // unique values are expected as count is positive + let randMembers = await client.zrandmemberWithCountWithScores( + key1, + 4, + ); + + for (const member of randMembers) { + const key = String(member[0]); + const score = Number(member[1]); + expect(score).toEqual(memberScoreMap.get(key)); + } + + // Duplicate values are expected as count is negative + randMembers = await client.zrandmemberWithCountWithScores( + key1, + -4, + ); + expect(randMembers.length).toBe(4); + const keys = []; + + for (const member of randMembers) { + keys.push(String(member[0])); + } + + expect(randMembers.length).not.toEqual(new Set(keys).size); + + // non existing key should return empty array + randMembers = await client.zrandmemberWithCountWithScores( + "nonExistingKey", + -4, + ); + expect(randMembers.length).toBe(0); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect( + client.zrandmemberWithCount(key2, 1), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 479c9da083..e13ba88666 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -450,6 +450,7 @@ export async function transactionTest( const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap const key20 = "{key}" + uuidv4(); // list + const key21 = "{key}" + uuidv4(); // zset random const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -862,6 +863,17 @@ export async function transactionTest( 'geohash(key18, ["Palermo", "Catania", "NonExisting"])', ["sqc8b49rny0", "sqdtr74hyu0", null], ]); + baseTransaction.zadd(key21, { one: 1.0 }); + responseData.push(["zadd(key21, {one: 1.0}", 1]); + baseTransaction.zrandmember(key21); + responseData.push(["zrandmember(key21)", "one"]); + baseTransaction.zrandmemberWithCount(key21, 1); + responseData.push(["zrandmemberWithCountWithScores(key21, 1)", "one"]); + baseTransaction.zrandmemberWithCountWithScores(key21, 1); + responseData.push([ + "zrandmemberWithCountWithScores(key21, 1)", + [Buffer.from("one"), 1.0], + ]); if (gte("6.2.0", version)) { baseTransaction From 20ea28dc05ce37df2e5e880cd84d186ae032bb8a Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 16:00:47 -0700 Subject: [PATCH 080/236] implement msetnx Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 24 ++++++++++++++++++++++++ node/src/Commands.ts | 12 ++++++++++++ node/src/Transaction.ts | 15 +++++++++++++++ node/tests/SharedTests.ts | 34 ++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 5 files changed, 87 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 3d0a6c284c..c24fd8af43 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -91,6 +91,7 @@ import { createLTrim, createMGet, createMSet, + createMSetNX, createObjectEncoding, createObjectFreq, createObjectIdletime, @@ -908,6 +909,29 @@ export class BaseClient { return this.createWritePromise(createMSet(keyValueMap)); } + /** + * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + * more keys already exist, the entire operation fails. + * + * See https://valkey.io/commands/msetnx/ for more details. + * + * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. + * @param keyValueMap - A key-value map consisting of keys and their respective values to set. + * @returns `True` if all keys were set. `False` if no key was set. + * + * @example + * ```typescript + * const result1 = await client.msetnx({"key1": "value1", "key2": "value2"}); + * console.log(result1); // Output: `True` + * + * const result2 = await client.msetnx({"key2": "value4", "key3": "value5"}); + * console.log(result2); // Output: `False` + * ``` + */ + public async msetnx(keyValueMap: Record): Promise { + return this.createWritePromise(createMSetNX(keyValueMap)); + } + /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. * See https://valkey.io/commands/incr/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e8377c9e47..6f209d42f7 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -343,6 +343,18 @@ export function createMSet( return createCommand(RequestType.MSet, Object.entries(keyValueMap).flat()); } +/** + * @internal + */ +export function createMSetNX( + keyValueMap: Record, +): command_request.Command { + return createCommand( + RequestType.MSetNX, + Object.entries(keyValueMap).flat(), + ); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 24be54d0a6..a979624dfa 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -105,6 +105,7 @@ import { createLolwut, createMGet, createMSet, + createMSetNX, createObjectEncoding, createObjectFreq, createObjectIdletime, @@ -347,6 +348,20 @@ export class BaseTransaction> { return this.addAndReturn(createMSet(keyValueMap)); } + /** + * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + * more keys already exist, the entire operation fails. + * + * See https://valkey.io/commands/msetnx/ for more details. + * + * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. + * @param keyValueMap - A key-value map consisting of keys and their respective values to set. + * @returns `True` if all keys were set. `False` if no key was set. + */ + public msetnx(keyValueMap: Record): T { + return this.addAndReturn(createMSetNX(keyValueMap)); + } + /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. * See https://valkey.io/commands/incr/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 203136507b..b06a3d1e68 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -42,6 +42,7 @@ import { intoArray, intoString, } from "./TestUtilities"; +import { check } from "prettier"; export type BaseClient = GlideClient | GlideClusterClient; @@ -316,6 +317,39 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `msetnx test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + const nonExistingKey = uuidv4(); + const value = uuidv4(); + const keyValueMap1 = { + [key1]: value, + [key2]: value, + }; + const keyValueMap2 = { + [key2]: value, + [key3]: value, + }; + + expect(await client.msetnx(keyValueMap1)).toEqual(true); + + checkSimple( + await client.mget([key1, key2, nonExistingKey]), + ).toEqual([value, value, null]); + + expect(await client.msetnx(keyValueMap2)).toEqual(false); + + expect(await client.get(key3)).toEqual(null); + checkSimple(await client.get(key2)).toEqual(value); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `incr, incrBy and incrByFloat with existing key_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 479c9da083..50a7ad2c72 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -484,6 +484,8 @@ export async function transactionTest( responseData.push(['customCommand(["MGET", key1, key2])', ["bar", "baz"]]); baseTransaction.mset({ [key3]: value }); responseData.push(["mset({ [key3]: value })", "OK"]); + baseTransaction.msetnx({ [key3]: value }); + responseData.push(["msetnx({ [key3]: value })", false]); baseTransaction.mget([key1, key2]); responseData.push(["mget([key1, key2])", ["bar", "baz"]]); baseTransaction.strlen(key1); From 98f825cbb5b9fdf207278c22794c4d5be2b17fc3 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 16:02:29 -0700 Subject: [PATCH 081/236] add changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0370e1b3..5551f204c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023)) * Java, Python: Update docs for GEOSEARCH command ([#2017](https://github.com/valkey-io/valkey-glide/pull/2017)) * Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019)) From 7963e9af647e913573b38e095bda74042922b3e5 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 16:05:18 -0700 Subject: [PATCH 082/236] remove import in shared tests Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b06a3d1e68..48338ada3e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -42,7 +42,6 @@ import { intoArray, intoString, } from "./TestUtilities"; -import { check } from "prettier"; export type BaseClient = GlideClient | GlideClusterClient; From 054cc3d79d38d5915043faa7a83740cfad04e106 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Mon, 29 Jul 2024 16:23:09 -0700 Subject: [PATCH 083/236] Java: add examples (#1925) * Added Java Standalone and Cluster examples. Signed-off-by: Yi-Pin Chen --- examples/java/README.md | 16 +- examples/java/build.gradle | 20 ++- .../java/glide/examples/ClusterExample.java | 151 ++++++++++++++++++ .../main/java/glide/examples/ExamplesApp.java | 40 ----- .../glide/examples/StandaloneExample.java | 138 ++++++++++++++++ 5 files changed, 309 insertions(+), 56 deletions(-) create mode 100644 examples/java/src/main/java/glide/examples/ClusterExample.java delete mode 100644 examples/java/src/main/java/glide/examples/ExamplesApp.java create mode 100644 examples/java/src/main/java/glide/examples/StandaloneExample.java diff --git a/examples/java/README.md b/examples/java/README.md index 0e46d8ba62..395ca7d1a7 100644 --- a/examples/java/README.md +++ b/examples/java/README.md @@ -1,18 +1,14 @@ ## Run -Ensure that you have an instance of Valkey running on "localhost" on "6379". Otherwise, update glide.examples.ExamplesApp with a configuration that matches your server settings. +Ensure that you have an instance of Valkey running on "localhost" on "6379". Otherwise, update glide.examples.StandaloneExample or glide.examples.ClusterExample with a configuration that matches your server settings. -To run the example: +To run the Standalone example: ``` cd valkey-glide/examples/java -./gradlew :run +./gradlew :runStandalone ``` - -You should expect to see the output: +To run the Cluster example: ``` -> Task :run -PING: PONG -PING(found you): found you -SET(apples, oranges): OK -GET(apples): oranges +cd valkey-glide/examples/java +./gradlew :runCluster ``` diff --git a/examples/java/build.gradle b/examples/java/build.gradle index fa55ac434e..6ff2785725 100644 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -1,6 +1,5 @@ plugins { - // Apply the application plugin to add support for building a CLI application in Java. - id 'application' + id "java" id "com.google.osdetector" version "1.7.3" } @@ -11,10 +10,19 @@ repositories { } dependencies { - implementation "io.valkey:valkey-glide:1.0.1:${osdetector.classifier}" + implementation "io.valkey:valkey-glide:1.+:${osdetector.classifier}" } -application { - // Define the main class for the application. - mainClass = 'glide.examples.ExamplesApp' +task runStandalone(type: JavaExec) { + group = 'application' + description = 'Run the standalone example' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'glide.examples.StandaloneExample' +} + +task runCluster(type: JavaExec) { + group = 'application' + description = 'Run the cluster example' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'glide.examples.ClusterExample' } diff --git a/examples/java/src/main/java/glide/examples/ClusterExample.java b/examples/java/src/main/java/glide/examples/ClusterExample.java new file mode 100644 index 0000000000..cc598b632a --- /dev/null +++ b/examples/java/src/main/java/glide/examples/ClusterExample.java @@ -0,0 +1,151 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.examples; + +import static glide.api.logging.Logger.Level.ERROR; +import static glide.api.logging.Logger.Level.INFO; +import static glide.api.logging.Logger.Level.WARN; +import static glide.api.logging.Logger.log; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; + +import glide.api.GlideClusterClient; +import glide.api.logging.Logger; +import glide.api.models.ClusterValue; +import glide.api.models.commands.InfoOptions; +import glide.api.models.configuration.GlideClusterClientConfiguration; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.exceptions.ClosingException; +import glide.api.models.exceptions.ConnectionException; +import glide.api.models.exceptions.TimeoutException; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class ClusterExample { + + /** + * Creates and returns a GlideClusterClient instance. + * + *

This function initializes a GlideClusterClient with the provided list of nodes. + * The list may contain the address of one or more cluster nodes, and the client will + * automatically discover all nodes in the cluster. + * + * @return A GlideClusterClient connected to the discovered nodes. + * @throws CancellationException if the operation is cancelled. + * @throws ExecutionException if the client fails due to execution errors. + * @throws InterruptedException if the operation is interrupted. + */ + public static GlideClusterClient createClient(List nodeList) + throws CancellationException, ExecutionException, InterruptedException { + // Check `GlideClusterClientConfiguration` for additional options. + GlideClusterClientConfiguration config = + GlideClusterClientConfiguration.builder() + .addresses(nodeList) + // Enable this field if the servers are configured with TLS. + // .useTLS(true); + .build(); + + GlideClusterClient client = GlideClusterClient.createClient(config).get(); + return client; + } + + /** + * Executes the main logic of the application, performing basic operations such as SET, GET, PING, + * and INFO REPLICATION using the provided GlideClusterClient. + * + * @param client An instance of GlideClusterClient. + * @throws ExecutionException if an execution error occurs during operations. + * @throws InterruptedException if the operation is interrupted. + */ + public static void appLogic(GlideClusterClient client) + throws ExecutionException, InterruptedException { + + // Send SET and GET + CompletableFuture setResponse = client.set("foo", "bar"); + log(INFO, "app", "Set response is " + setResponse.get()); + + CompletableFuture getResponse = client.get("foo"); + log(INFO, "app", "Get response is " + getResponse.get()); + + // Send PING to all primaries (according to Valkey's PING request_policy) + CompletableFuture pong = client.ping(); + log(INFO, "app", "Ping response is " + pong.get()); + + // Send INFO REPLICATION with routing option to all nodes + ClusterValue infoResponse = + client + .info(InfoOptions.builder().section(InfoOptions.Section.REPLICATION).build(), ALL_NODES) + .get(); + log( + INFO, + "app", + "INFO REPLICATION responses from all nodes are " + infoResponse.getMultiValue()); + } + + /** + * Executes the application logic with exception handling. + * + * @throws ExecutionException if an execution error occurs during operations. + */ + private static void execAppLogic() throws ExecutionException { + + // Define list of nodes + List nodeList = + Collections.singletonList(NodeAddress.builder().host("localhost").port(6379).build()); + + while (true) { + try (GlideClusterClient client = createClient(nodeList)) { + appLogic(client); + return; + } catch (CancellationException e) { + log(ERROR, "glide", "Request cancelled: " + e.getMessage()); + throw e; + } catch (InterruptedException e) { + log(ERROR, "glide", "Client interrupted: " + e.getMessage()); + Thread.currentThread().interrupt(); // Restore interrupt status + throw new CancellationException("Client was interrupted."); + } catch (ExecutionException e) { + // All Glide errors will be handled as ExecutionException + if (e.getCause() instanceof ClosingException) { + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + if (e.getMessage().contains("NOAUTH")) { + log(ERROR, "glide", "Authentication error encountered: " + e.getMessage()); + throw e; + } else { + log(WARN, "glide", "Client has closed and needs to be re-created: " + e.getMessage()); + } + } else if (e.getCause() instanceof ConnectionException) { + // The client wasn't able to reestablish the connection within the given retries + log(ERROR, "glide", "Connection error encountered: " + e.getMessage()); + throw e; + } else if (e.getCause() instanceof TimeoutException) { + // A request timed out. You may choose to retry the execution based on your application's + // logic + log(ERROR, "glide", "Timeout encountered: " + e.getMessage()); + throw e; + } else { + log(ERROR, "glide", "Execution error encountered: " + e.getCause()); + throw e; + } + } + } + } + + /** + * The entry point of the cluster example. This method sets up the logger configuration and + * executes the main application logic. + * + * @param args Command-line arguments passed to the application. + * @throws ExecutionException if an error occurs during execution of the application logic. + */ + public static void main(String[] args) throws ExecutionException { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(INFO); + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + execAppLogic(); + } +} diff --git a/examples/java/src/main/java/glide/examples/ExamplesApp.java b/examples/java/src/main/java/glide/examples/ExamplesApp.java deleted file mode 100644 index 4a686786eb..0000000000 --- a/examples/java/src/main/java/glide/examples/ExamplesApp.java +++ /dev/null @@ -1,40 +0,0 @@ -/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.examples; - -import glide.api.GlideClient; -import glide.api.models.configuration.GlideClientConfiguration; -import glide.api.models.configuration.NodeAddress; -import java.util.concurrent.ExecutionException; - -public class ExamplesApp { - - // main application entrypoint - public static void main(String[] args) { - runGlideExamples(); - } - - private static void runGlideExamples() { - String host = "localhost"; - Integer port = 6379; - boolean useSsl = false; - - GlideClientConfiguration config = - GlideClientConfiguration.builder() - .address(NodeAddress.builder().host(host).port(port).build()) - .useTLS(useSsl) - .build(); - - try (GlideClient client = GlideClient.createClient(config).get()) { - - System.out.println("PING: " + client.ping().get()); - System.out.println("PING(found you): " + client.ping("found you").get()); - - System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); - System.out.println("GET(apples): " + client.get("apples").get()); - - } catch (ExecutionException | InterruptedException e) { - System.out.println("Glide example failed with an exception: "); - e.printStackTrace(); - } - } -} diff --git a/examples/java/src/main/java/glide/examples/StandaloneExample.java b/examples/java/src/main/java/glide/examples/StandaloneExample.java new file mode 100644 index 0000000000..996e408e83 --- /dev/null +++ b/examples/java/src/main/java/glide/examples/StandaloneExample.java @@ -0,0 +1,138 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.examples; + +import static glide.api.logging.Logger.Level.ERROR; +import static glide.api.logging.Logger.Level.INFO; +import static glide.api.logging.Logger.Level.WARN; +import static glide.api.logging.Logger.log; + +import glide.api.GlideClient; +import glide.api.logging.Logger; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.exceptions.ClosingException; +import glide.api.models.exceptions.ConnectionException; +import glide.api.models.exceptions.TimeoutException; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class StandaloneExample { + + /** + * Creates and returns a GlideClient instance. + * + *

This function initializes a GlideClient with the provided list of nodes. The + * list may contain either only primary node or a mix of primary and replica nodes. The + * GlideClient + * use these nodes to connect to the Standalone setup servers. + * + * @return A GlideClient connected to the provided node address. + * @throws CancellationException if the operation is cancelled. + * @throws ExecutionException if the client fails due to execution errors. + * @throws InterruptedException if the operation is interrupted. + */ + public static GlideClient createClient(List nodeList) + throws CancellationException, ExecutionException, InterruptedException { + // Check `GlideClientConfiguration` for additional options. + GlideClientConfiguration config = + GlideClientConfiguration.builder() + .addresses(nodeList) + // Enable this field if the servers are configured with TLS. + // .useTLS(true); + .build(); + + GlideClient client = GlideClient.createClient(config).get(); + return client; + } + + /** + * Executes the main logic of the application, performing basic operations such as SET, GET, and + * PING using the provided GlideClient. + * + * @param client An instance of GlideClient. + * @throws ExecutionException if an execution error occurs during operations. + * @throws InterruptedException if the operation is interrupted. + */ + public static void appLogic(GlideClient client) throws ExecutionException, InterruptedException { + + // Send SET and GET + CompletableFuture setResponse = client.set("foo", "bar"); + log(INFO, "app", "Set response is " + setResponse.get()); + + CompletableFuture getResponse = client.get("foo"); + log(INFO, "app", "Get response is " + getResponse.get()); + + // Send PING + CompletableFuture pong = client.ping(); + log(INFO, "app", "Ping response is " + pong.get()); + } + + /** + * Executes the application logic with exception handling. + * + * @throws ExecutionException if an execution error occurs during operations. + */ + private static void execAppLogic() throws ExecutionException { + + // Define list of nodes + List nodeList = + Collections.singletonList(NodeAddress.builder().host("localhost").port(6379).build()); + + while (true) { + try (GlideClient client = createClient(nodeList)) { + appLogic(client); + return; + } catch (CancellationException e) { + log(ERROR, "glide", "Request cancelled: " + e.getMessage()); + throw e; + } catch (InterruptedException e) { + log(ERROR, "glide", "Client interrupted: " + e.getMessage()); + Thread.currentThread().interrupt(); // Restore interrupt status + throw new CancellationException("Client was interrupted."); + } catch (ExecutionException e) { + // All Glide errors will be handled as ExecutionException + if (e.getCause() instanceof ClosingException) { + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + if (e.getMessage().contains("NOAUTH")) { + log(ERROR, "glide", "Authentication error encountered: " + e.getMessage()); + throw e; + } else { + log(WARN, "glide", "Client has closed and needs to be re-created: " + e.getMessage()); + } + } else if (e.getCause() instanceof ConnectionException) { + // The client wasn't able to reestablish the connection within the given retries + log(ERROR, "glide", "Connection error encountered: " + e.getMessage()); + throw e; + } else if (e.getCause() instanceof TimeoutException) { + // A request timed out. You may choose to retry the execution based on your application's + // logic + log(ERROR, "glide", "Timeout encountered: " + e.getMessage()); + throw e; + } else { + log(ERROR, "glide", "Execution error encountered: " + e.getCause()); + throw e; + } + } + } + } + + /** + * The entry point of the standalone example. This method sets up the logger configuration and + * executes the main application logic. + * + * @param args Command-line arguments passed to the application. + * @throws ExecutionException if an error occurs during execution of the application logic. + */ + public static void main(String[] args) throws ExecutionException { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(INFO); + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + execAppLogic(); + } +} From aca6146114636de4f167fa784c4b510237862bcf Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 16:23:51 -0700 Subject: [PATCH 084/236] add cross slot test and address comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 8 ++++---- node/src/Transaction.ts | 2 +- node/tests/RedisClusterClient.test.ts | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index c24fd8af43..8a9fd22910 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -917,18 +917,18 @@ export class BaseClient { * * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. * @param keyValueMap - A key-value map consisting of keys and their respective values to set. - * @returns `True` if all keys were set. `False` if no key was set. + * @returns `true` if all keys were set. `false` if no key was set. * * @example * ```typescript * const result1 = await client.msetnx({"key1": "value1", "key2": "value2"}); - * console.log(result1); // Output: `True` + * console.log(result1); // Output: `true` * * const result2 = await client.msetnx({"key2": "value4", "key3": "value5"}); - * console.log(result2); // Output: `False` + * console.log(result2); // Output: `false` * ``` */ - public async msetnx(keyValueMap: Record): Promise { + public async msetnx(keyValueMap: Record): Promise { return this.createWritePromise(createMSetNX(keyValueMap)); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index a979624dfa..3cfd600c1c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -356,7 +356,7 @@ export class BaseTransaction> { * * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. * @param keyValueMap - A key-value map consisting of keys and their respective values to set. - * @returns `True` if all keys were set. `False` if no key was set. + * @returns `true` if all keys were set. `false` if no key was set. */ public msetnx(keyValueMap: Record): T { return this.addAndReturn(createMSetNX(keyValueMap)); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 9b29d6196a..eefeb49c9b 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -308,6 +308,7 @@ describe("GlideClusterClient", () => { const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), + client.msetnx({"abc": "xyz"}), client.brpop(["abc", "zxy", "lkn"], 0.1), client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), From aea7f979d8730d79cd92b00997d08191ae5ce759 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 16:26:34 -0700 Subject: [PATCH 085/236] ran linter Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index eefeb49c9b..eb53edfde9 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -308,7 +308,7 @@ describe("GlideClusterClient", () => { const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), - client.msetnx({"abc": "xyz"}), + client.msetnx({ abc: "xyz" }), client.brpop(["abc", "zxy", "lkn"], 0.1), client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), From 089be122a44ef270f56502283475925829037bd7 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Mon, 29 Jul 2024 16:51:16 -0700 Subject: [PATCH 086/236] Node: added COPY command (#2024) * Node: added COPY command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 8 +-- node/src/Commands.ts | 24 +++++++ node/src/GlideClient.ts | 43 ++++++++++++ node/src/GlideClusterClient.ts | 32 +++++++++ node/src/Transaction.ts | 57 +++++++++++++++- node/tests/RedisClient.test.ts | 90 +++++++++++++++++++++++++ node/tests/RedisClientInternals.test.ts | 6 +- node/tests/RedisClusterClient.test.ts | 53 ++++++++++++++- node/tests/SharedTests.ts | 2 +- node/tests/TestUtilities.ts | 2 +- 11 files changed, 306 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3271ffb9d9..3c6c58b50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990)) * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) * Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) +* Node: Added COPY command ([#2024](https://github.com/valkey-io/valkey-glide/pull/2024)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 78f7fc92b9..d4b4788b68 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -11,8 +11,8 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, - BitmapIndexType, BitOffsetOptions, + BitmapIndexType, BitwiseOperation, CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, @@ -21,13 +21,13 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, - GeospatialData, GeoUnit, + GeospatialData, InsertPosition, KeyWeight, - MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars LPosOptions, ListDirection, + MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars RangeByIndex, RangeByLex, RangeByScore, @@ -41,9 +41,9 @@ import { ZAddOptions, createBLPop, createBRPop, + createBZMPop, createBitCount, createBitOp, - createBZMPop, createBitPos, createDecr, createDecrBy, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 19f8ceed2e..7361871411 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2017,6 +2017,30 @@ export function createFlushDB(mode?: FlushMode): command_request.Command { } } +/** + * + * @internal + */ +export function createCopy( + source: string, + destination: string, + options?: { destinationDB?: number; replace?: boolean }, +): command_request.Command { + let args: string[] = [source, destination]; + + if (options) { + if (options.destinationDB !== undefined) { + args = args.concat("DB", options.destinationDB.toString()); + } + + if (options.replace) { + args.push("REPLACE"); + } + } + + return createCommand(RequestType.Copy, args); +} + /** * Optional arguments to LPOS command. * diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index c05126fc58..09af14ed92 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -21,6 +21,7 @@ import { createConfigResetStat, createConfigRewrite, createConfigSet, + createCopy, createCustomCommand, createDBSize, createEcho, @@ -374,6 +375,48 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createTime()); } + /** + * Copies the value stored at the `source` to the `destination` key. If `destinationDB` is specified, + * the value will be copied to the database specified, otherwise the current database will be used. + * When `replace` is true, removes the `destination` key first if it already exists, otherwise performs + * no action. + * + * See https://valkey.io/commands/copy/ for more details. + * + * @param source - The key to the source value. + * @param destination - The key where the value should be copied to. + * @param destinationDB - (Optional) The alternative logical database index for the destination key. + * If not provided, the current database will be used. + * @param replace - (Optional) If `true`, the `destination` key should be removed before copying the + * value to it. If not provided, no action will be performed if the key already exists. + * @returns `true` if `source` was copied, `false` if the `source` was not copied. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * const result = await client.copy("set1", "set2"); + * console.log(result); // Output: true - "set1" was copied to "set2". + * ``` + * ```typescript + * const result = await client.copy("set1", "set2", { replace: true }); + * console.log(result); // Output: true - "set1" was copied to "set2". + * ``` + * ```typescript + * const result = await client.copy("set1", "set2", { destinationDB: 1, replace: false }); + * console.log(result); // Output: true - "set1" was copied to "set2". + * ``` + */ + public async copy( + source: string, + destination: string, + options?: { destinationDB?: number; replace?: boolean }, + ): Promise { + return this.createWritePromise( + createCopy(source, destination, options), + ); + } + /** * Displays a piece of generative computer art and the server version. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 10e8d87a38..c6b47226f0 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -21,6 +21,7 @@ import { createConfigResetStat, createConfigRewrite, createConfigSet, + createCopy, createCustomCommand, createDBSize, createEcho, @@ -638,6 +639,37 @@ export class GlideClusterClient extends BaseClient { return this.createWritePromise(createTime(), toProtobufRoute(route)); } + /** + * Copies the value stored at the `source` to the `destination` key. When `replace` is `true`, + * removes the `destination` key first if it already exists, otherwise performs no action. + * + * See https://valkey.io/commands/copy/ for more details. + * + * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. + * @param source - The key to the source value. + * @param destination - The key where the value should be copied to. + * @param replace - (Optional) If `true`, the `destination` key should be removed before copying the + * value to it. If not provided, no action will be performed if the key already exists. + * @returns `true` if `source` was copied, `false` if the `source` was not copied. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * const result = await client.copy("set1", "set2", true); + * console.log(result); // Output: true - "set1" was copied to "set2". + * ``` + */ + public async copy( + source: string, + destination: string, + replace?: boolean, + ): Promise { + return this.createWritePromise( + createCopy(source, destination, { replace: replace }), + ); + } + /** * Displays a piece of generative computer art and the server version. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 073194bc20..2cb2f6499b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -17,8 +17,8 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, - GeospatialData, GeoUnit, + GeospatialData, InfoOptions, InsertPosition, KeyWeight, @@ -49,6 +49,7 @@ import { createConfigResetStat, createConfigRewrite, createConfigSet, + createCopy, createCustomCommand, createDBSize, createDecr, @@ -154,6 +155,7 @@ import { createZDiff, createZDiffStore, createZDiffWithScores, + createZIncrBy, createZInterCard, createZInterstore, createZMPop, @@ -170,7 +172,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - createZIncrBy, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2436,6 +2437,33 @@ export class Transaction extends BaseTransaction { public select(index: number): Transaction { return this.addAndReturn(createSelect(index)); } + + /** + * Copies the value stored at the `source` to the `destination` key. If `destinationDB` is specified, + * the value will be copied to the database specified, otherwise the current database will be used. + * When `replace` is true, removes the `destination` key first if it already exists, otherwise performs + * no action. + * + * See https://valkey.io/commands/copy/ for more details. + * + * @param source - The key to the source value. + * @param destination - The key where the value should be copied to. + * @param destinationDB - (Optional) The alternative logical database index for the destination key. + * If not provided, the current database will be used. + * @param replace - (Optional) If `true`, the `destination` key should be removed before copying the + * value to it. If not provided, no action will be performed if the key already exists. + * + * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. + * + * since Valkey version 6.2.0. + */ + public copy( + source: string, + destination: string, + options?: { destinationDB?: number; replace?: boolean }, + ): Transaction { + return this.addAndReturn(createCopy(source, destination, options)); + } } /** @@ -2451,4 +2479,29 @@ export class Transaction extends BaseTransaction { */ export class ClusterTransaction extends BaseTransaction { /// TODO: add all CLUSTER commands + + /** + * Copies the value stored at the `source` to the `destination` key. When `replace` is true, + * removes the `destination` key first if it already exists, otherwise performs no action. + * + * See https://valkey.io/commands/copy/ for more details. + * + * @param source - The key to the source value. + * @param destination - The key where the value should be copied to. + * @param replace - (Optional) If `true`, the `destination` key should be removed before copying the + * value to it. If not provided, no action will be performed if the key already exists. + * + * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. + * + * since Valkey version 6.2.0. + */ + public copy( + source: string, + destination: string, + replace?: boolean, + ): ClusterTransaction { + return this.addAndReturn( + createCopy(source, destination, { replace: replace }), + ); + } } diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index fb4b3e4bd8..dab8ca6edb 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -366,6 +366,96 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "copy with DB test_%p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const source = `{key}-${uuidv4()}`; + const destination = `{key}-${uuidv4()}`; + const value1 = uuidv4(); + const value2 = uuidv4(); + const index0 = 0; + const index1 = 1; + const index2 = 2; + + // neither key exists + expect( + await client.copy(source, destination, { + destinationDB: index1, + replace: false, + }), + ).toEqual(false); + + // source exists, destination does not + expect(await client.set(source, value1)).toEqual("OK"); + expect( + await client.copy(source, destination, { + destinationDB: index1, + replace: false, + }), + ).toEqual(true); + expect(await client.select(index1)).toEqual("OK"); + checkSimple(await client.get(destination)).toEqual(value1); + + // new value for source key + expect(await client.select(index0)).toEqual("OK"); + expect(await client.set(source, value2)).toEqual("OK"); + + // no REPLACE, copying to existing key on DB 1, non-existing key on DB 2 + expect( + await client.copy(source, destination, { + destinationDB: index1, + replace: false, + }), + ).toEqual(false); + expect( + await client.copy(source, destination, { + destinationDB: index2, + replace: false, + }), + ).toEqual(true); + + // new value only gets copied to DB 2 + expect(await client.select(index1)).toEqual("OK"); + checkSimple(await client.get(destination)).toEqual(value1); + expect(await client.select(index2)).toEqual("OK"); + checkSimple(await client.get(destination)).toEqual(value2); + + // both exists, with REPLACE, when value isn't the same, source always get copied to + // destination + expect(await client.select(index0)).toEqual("OK"); + expect( + await client.copy(source, destination, { + destinationDB: index1, + replace: true, + }), + ).toEqual(true); + expect(await client.select(index1)).toEqual("OK"); + checkSimple(await client.get(destination)).toEqual(value2); + + //transaction tests + const transaction = new Transaction(); + transaction.select(index1); + transaction.set(source, value1); + transaction.copy(source, destination, { + destinationDB: index1, + replace: true, + }); + transaction.get(destination); + const results = await client.exec(transaction); + + checkSimple(results).toEqual(["OK", "OK", true, value1]); + + client.close(); + }, + TIMEOUT, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "function load test_%p", async (protocol) => { diff --git a/node/tests/RedisClientInternals.test.ts b/node/tests/RedisClientInternals.test.ts index 888b47c374..631c936513 100644 --- a/node/tests/RedisClientInternals.test.ts +++ b/node/tests/RedisClientInternals.test.ts @@ -22,6 +22,7 @@ import { BaseClientConfiguration, ClosingError, ClusterClientConfiguration, + ClusterTransaction, GlideClient, GlideClientConfiguration, GlideClusterClient, @@ -30,7 +31,6 @@ import { RequestError, ReturnType, SlotKeyTypes, - Transaction, } from ".."; import { command_request, @@ -376,7 +376,7 @@ describe("SocketConnectionInternals", () => { sendResponse(socket, ResponseType.OK, request.callbackIdx); }); - const transaction = new Transaction(); + const transaction = new ClusterTransaction(); transaction.set("key", "value"); const slotKey: SlotKeyTypes = { type: "primarySlotKey", @@ -408,7 +408,7 @@ describe("SocketConnectionInternals", () => { value: "# Server", }); }); - const transaction = new Transaction(); + const transaction = new ClusterTransaction(); transaction.info([InfoOptions.Server]); const result = await connection.exec(transaction, "randomNode"); expect(intoString(result)).toEqual( diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 9b29d6196a..0f7281746d 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -22,8 +22,8 @@ import { Routes, ScoreFilter, } from ".."; -import { FlushMode } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -327,6 +327,7 @@ describe("GlideClusterClient", () => { client.zdiff(["abc", "zxy", "lkn"]), client.zdiffWithScores(["abc", "zxy", "lkn"]), client.zdiffstore("abc", ["zxy", "lkn"]), + client.copy("abc", "zxy", true), ); } @@ -539,6 +540,56 @@ describe("GlideClusterClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "copy test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const source = `{key}-${uuidv4()}`; + const destination = `{key}-${uuidv4()}`; + const value1 = uuidv4(); + const value2 = uuidv4(); + + // neither key exists + expect(await client.copy(source, destination, true)).toEqual(false); + expect(await client.copy(source, destination)).toEqual(false); + + // source exists, destination does not + expect(await client.set(source, value1)).toEqual("OK"); + expect(await client.copy(source, destination, false)).toEqual(true); + checkSimple(await client.get(destination)).toEqual(value1); + + // new value for source key + expect(await client.set(source, value2)).toEqual("OK"); + + // both exists, no REPLACE + expect(await client.copy(source, destination)).toEqual(false); + expect(await client.copy(source, destination, false)).toEqual( + false, + ); + checkSimple(await client.get(destination)).toEqual(value1); + + // both exists, with REPLACE + expect(await client.copy(source, destination, true)).toEqual(true); + checkSimple(await client.get(destination)).toEqual(value2); + + //transaction tests + const transaction = new ClusterTransaction(); + transaction.set(source, value1); + transaction.copy(source, destination, true); + transaction.get(destination); + const results = await client.exec(transaction); + + checkSimple(results).toEqual(["OK", true, value1]); + + client.close(); + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "flushdb flushall dbsize test_%p", async (protocol) => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index cb43d3ce51..dc05269622 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -18,11 +18,11 @@ import { FlushMode, GeoUnit, GeospatialData, - ListDirection, GlideClient, GlideClusterClient, InfoOptions, InsertPosition, + ListDirection, ProtocolVersion, RequestError, ScoreFilter, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e13ba88666..fd2dd722d7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -5,8 +5,8 @@ import { expect } from "@jest/globals"; import { exec } from "child_process"; import parseArgs from "minimist"; -import { v4 as uuidv4 } from "uuid"; import { gte } from "semver"; +import { v4 as uuidv4 } from "uuid"; import { BaseClient, BaseClientConfiguration, From 70d4fca096315cf882127d073434acff40f2865e Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 18:47:57 -0700 Subject: [PATCH 087/236] add more to cross slot Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index eb53edfde9..510cae59ff 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -308,7 +308,7 @@ describe("GlideClusterClient", () => { const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), - client.msetnx({ abc: "xyz" }), + client.msetnx({ abc: "xyz" , def: "abc"}), client.brpop(["abc", "zxy", "lkn"], 0.1), client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), From 9cfb16965d4c2f086c3d0d3a5a50293ab50747f1 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 18:48:12 -0700 Subject: [PATCH 088/236] add more to cross slot Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 510cae59ff..241153e639 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -308,7 +308,7 @@ describe("GlideClusterClient", () => { const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), - client.msetnx({ abc: "xyz" , def: "abc"}), + client.msetnx({ abc: "xyz" , def: "abc", hij: "def"}), client.brpop(["abc", "zxy", "lkn"], 0.1), client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), From cc7b05c7eb63eaa9695ce393c5bb7d9681797c1b Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 29 Jul 2024 18:54:55 -0700 Subject: [PATCH 089/236] ran linter Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 241153e639..0697c5323f 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -308,7 +308,7 @@ describe("GlideClusterClient", () => { const promises: Promise[] = [ client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), - client.msetnx({ abc: "xyz" , def: "abc", hij: "def"}), + client.msetnx({ abc: "xyz", def: "abc", hij: "def" }), client.brpop(["abc", "zxy", "lkn"], 0.1), client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), From eb1bbcfd8ebb5b2f588b0d3ba8287ba9bb8d71f0 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 30 Jul 2024 00:11:27 -0700 Subject: [PATCH 090/236] Node: Fix version check - again. (#2045) Fix version check. Signed-off-by: Yury-Fridlyand --- node/jest.config.js | 2 +- node/tests/RedisClient.test.ts | 2 +- node/tests/RedisClusterClient.test.ts | 2 +- node/tests/TestUtilities.ts | 18 +++++------ utils/TestUtils.ts | 45 +++++++++++++++++---------- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/node/jest.config.js b/node/jest.config.js index f47356fab9..1779b005b4 100644 --- a/node/jest.config.js +++ b/node/jest.config.js @@ -13,7 +13,7 @@ module.exports = { "cjs", "mjs", ], - testTimeout: 20000, + testTimeout: 600000, reporters: [ "default", [ diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index dab8ca6edb..d0eb710a60 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -48,7 +48,7 @@ describe("GlideClient", () => { parseCommandLineArgs()["standalone-endpoints"]; // Connect to cluster or create a new one based on the parsed addresses cluster = standaloneAddresses - ? RedisCluster.initFromExistingCluster( + ? await RedisCluster.initFromExistingCluster( parseEndpoints(standaloneAddresses), ) : await RedisCluster.createCluster(false, 1, 1); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 0f7281746d..31b8176284 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -54,7 +54,7 @@ describe("GlideClusterClient", () => { const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; // Connect to cluster or create a new one based on the parsed addresses cluster = clusterAddresses - ? RedisCluster.initFromExistingCluster( + ? await RedisCluster.initFromExistingCluster( parseEndpoints(clusterAddresses), ) : // setting replicaCount to 1 to facilitate tests routed to replicas diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fd2dd722d7..2db39cb409 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -531,7 +531,7 @@ export async function transactionTest( baseTransaction.lrange(key5, 0, -1); responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]); - if (gte("6.2.0", version)) { + if (gte(version, "6.2.0")) { baseTransaction.lmove( key5, key20, @@ -613,7 +613,7 @@ export async function transactionTest( baseTransaction.sismember(key7, "bar"); responseData.push(['sismember(key7, "bar")', true]); - if (gte("6.2.0", version)) { + if (gte(version, "6.2.0")) { baseTransaction.smismember(key7, ["bar", "foo", "baz"]); responseData.push([ 'smismember(key7, ["bar", "foo", "baz"])', @@ -642,7 +642,7 @@ export async function transactionTest( baseTransaction.zrank(key8, "member1"); responseData.push(['zrank(key8, "member1")', 0]); - if (gte("7.2.0", version)) { + if (gte(version, "7.2.0")) { baseTransaction.zrankWithScore(key8, "member1"); responseData.push(['zrankWithScore(key8, "member1")', [0, 1]]); } @@ -650,7 +650,7 @@ export async function transactionTest( baseTransaction.zrevrank(key8, "member5"); responseData.push(['zrevrank(key8, "member5")', 0]); - if (gte("7.2.0", version)) { + if (gte(version, "7.2.0")) { baseTransaction.zrevrankWithScore(key8, "member5"); responseData.push(['zrevrankWithScore(key8, "member5")', [0, 5]]); } @@ -681,7 +681,7 @@ export async function transactionTest( baseTransaction.zadd(key13, { one: 1, two: 2, three: 3.5 }); responseData.push(["zadd(key13, { one: 1, two: 2, three: 3.5 })", 3]); - if (gte("6.2.0", version)) { + if (gte(version, "6.2.0")) { baseTransaction.zdiff([key13, key12]); responseData.push(["zdiff([key13, key12])", ["three"]]); baseTransaction.zdiffWithScores([key13, key12]); @@ -712,7 +712,7 @@ export async function transactionTest( ); responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty - if (gte("7.0.0", version)) { + if (gte(version, "7.0.0")) { baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); responseData.push(["zadd(key14, { one: 1.0, two: 2.0 })", 2]); baseTransaction.zintercard([key8, key14]); @@ -814,7 +814,7 @@ export async function transactionTest( baseTransaction.get(key19); responseData.push(["get(key19)", "`bc`ab"]); - if (gte("7.0.0", version)) { + if (gte(version, "7.0.0")) { baseTransaction.bitcount(key17, { start: 5, end: 30, @@ -875,7 +875,7 @@ export async function transactionTest( [Buffer.from("one"), 1.0], ]); - if (gte("6.2.0", version)) { + if (gte(version, "6.2.0")) { baseTransaction .geosearch( key18, @@ -972,7 +972,7 @@ export async function transactionTest( true, ); - if (gte("7.0.0", version)) { + if (gte(version, "7.0.0")) { baseTransaction.functionLoad(code); responseData.push(["functionLoad(code)", libName]); baseTransaction.functionLoad(code, true); diff --git a/utils/TestUtils.ts b/utils/TestUtils.ts index e2f0f79d76..84f9acd146 100644 --- a/utils/TestUtils.ts +++ b/utils/TestUtils.ts @@ -40,21 +40,26 @@ export class RedisCluster { private clusterFolder: string | undefined; private version: string; - private constructor(addresses: [string, number][], clusterFolder?: string) { + private constructor( + version: string, + addresses: [string, number][], + clusterFolder?: string + ) { this.addresses = addresses; this.clusterFolder = clusterFolder; - this.version = RedisCluster.detectVersion(); + this.version = version; } - private static detectVersion(): string { - exec(`redis-server -v`, (error, stdout) => { - if (error) { - throw error; - } else { - return stdout.split("v=")[1].split(" ")[0]; - } - }); - return "0.0.0"; // unreachable; + private static async detectVersion(): Promise { + return new Promise((resolve, reject) => + exec(`redis-server -v`, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout.split("v=")[1].split(" ")[0]); + } + }) + ); } public static createCluster( @@ -93,17 +98,25 @@ export class RedisCluster { } else { const { clusterFolder, addresses: ports } = parseOutput(stdout); - resolve(new RedisCluster(ports, clusterFolder)); + + resolve( + RedisCluster.detectVersion().then( + (ver) => + new RedisCluster(ver, ports, clusterFolder) + ) + ); } } ); }); } - public static initFromExistingCluster( + public static async initFromExistingCluster( addresses: [string, number][] - ): RedisCluster { - return new RedisCluster(addresses, ""); + ): Promise { + return RedisCluster.detectVersion().then( + (ver) => new RedisCluster(ver, addresses, "") + ); } public ports(): number[] { @@ -119,7 +132,7 @@ export class RedisCluster { } public checkIfServerVersionLessThan(minVersion: string): boolean { - return lt(minVersion, this.version); + return lt(this.version, minVersion); } public async close() { From d2a1a928e5e8e6fbbe930835d2ee884d34a32a30 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Tue, 30 Jul 2024 00:21:33 +0000 Subject: [PATCH 091/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 4 +- java/THIRD_PARTY_LICENSES_JAVA | 4 +- node/THIRD_PARTY_LICENSES_NODE | 8 +- python/THIRD_PARTY_LICENSES_PYTHON | 440 ++++++++++++++++++++++++++- 4 files changed, 446 insertions(+), 10 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index ddcf381da8..d092cb25c5 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -14750,7 +14750,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.1 +Package: object:0.36.2 The following copyrights and licenses were found in the source code of this package: @@ -25070,7 +25070,7 @@ the following restrictions: ---- -Package: tokio:1.39.1 +Package: tokio:1.39.2 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 1c5176b1ed..39d0662505 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -15645,7 +15645,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.1 +Package: object:0.36.2 The following copyrights and licenses were found in the source code of this package: @@ -25965,7 +25965,7 @@ the following restrictions: ---- -Package: tokio:1.39.1 +Package: tokio:1.39.2 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 3d9f710c22..51b92788b3 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -15459,7 +15459,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.1 +Package: object:0.36.2 The following copyrights and licenses were found in the source code of this package: @@ -27153,7 +27153,7 @@ the following restrictions: ---- -Package: tokio:1.39.1 +Package: tokio:1.39.2 The following copyrights and licenses were found in the source code of this package: @@ -37493,7 +37493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: undici-types:5.26.5 +Package: undici-types:6.11.1 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:20.14.12 +Package: @types:node:22.0.0 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 3cb41c8027..0f992994e4 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -15441,7 +15441,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.1 +Package: object:0.36.2 The following copyrights and licenses were found in the source code of this package: @@ -27359,7 +27359,7 @@ the following restrictions: ---- -Package: tokio:1.39.1 +Package: tokio:1.39.2 The following copyrights and licenses were found in the source code of this package: @@ -36479,6 +36479,37 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: markupsafe:2.1.5 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: maturin:0.13.0 The following copyrights and licenses were found in the source code of this package: @@ -38290,6 +38321,411 @@ The following copyrights and licenses were found in the source code of this pack ---- +Package: pytest-html:4.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: pytest-metadata:3.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +---- + Package: requests:2.32.3 The following copyrights and licenses were found in the source code of this package: From 3821630bbe2b20de349e0acecdf3444350ca00df Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:34:48 -0700 Subject: [PATCH 092/236] Node: add BITFIELD and BITFIELD_RO commands (#2026) --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 28 +- node/src/BaseClient.ts | 71 +++++ node/src/Commands.ts | 294 +++++++++++++++++++++ node/src/Transaction.ts | 52 ++++ node/tests/SharedTests.ts | 287 ++++++++++++++++++++ node/tests/TestUtilities.ts | 25 ++ python/python/glide/async_commands/core.py | 2 +- 8 files changed, 757 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c6c58b50f..c8ea272755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added BITPOS command ([#1998](https://github.com/valkey-io/valkey-glide/pull/1998)) +* Node: Added BITFIELD and BITFIELD_RO commands ([#2026](https://github.com/valkey-io/valkey-glide/pull/2026)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index b22818940d..08dcbb8021 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -74,8 +74,18 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { - BitmapIndexType, + BitEncoding, + BitFieldGet, + BitFieldIncrBy, + BitFieldOffset, + BitFieldOverflow, + BitFieldSet, + BitFieldSubCommands, + BitOffset, + BitOffsetMultiplier, BitOffsetOptions, + BitOverflowControl, + BitmapIndexType, BitwiseOperation, ConditionalChange, GeoAddOptions, @@ -135,6 +145,8 @@ function initialize() { Transaction, PubSubMsg, ScoreFilter, + SignedEncoding, + UnsignedEncoding, createLeakedArray, createLeakedAttribute, createLeakedBigint, @@ -145,8 +157,18 @@ function initialize() { } = nativeBinding; module.exports = { - BitmapIndexType, + BitEncoding, + BitFieldGet, + BitFieldIncrBy, + BitFieldOffset, + BitFieldOverflow, + BitFieldSet, + BitFieldSubCommands, + BitOffset, + BitOffsetMultiplier, BitOffsetOptions, + BitOverflowControl, + BitmapIndexType, BitwiseOperation, ConditionalChange, GeoAddOptions, @@ -206,6 +228,8 @@ function initialize() { Transaction, PubSubMsg, ScoreFilter, + SignedEncoding, + UnsignedEncoding, createLeakedArray, createLeakedAttribute, createLeakedBigint, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d4b4788b68..668b72ec5d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -11,6 +11,13 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BitFieldGet, + BitFieldIncrBy, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldOverflow, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldSet, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldSubCommands, + BitOffset, // eslint-disable-line @typescript-eslint/no-unused-vars + BitOffsetMultiplier, // eslint-disable-line @typescript-eslint/no-unused-vars BitOffsetOptions, BitmapIndexType, BitwiseOperation, @@ -43,6 +50,7 @@ import { createBRPop, createBZMPop, createBitCount, + createBitField, createBitOp, createBitPos, createDecr, @@ -1158,6 +1166,69 @@ export class BaseClient { ); } + /** + * Reads or modifies the array of bits representing the string that is held at `key` based on the specified + * `subcommands`. + * + * See https://valkey.io/commands/bitfield/ for more details. + * + * @param key - The key of the string. + * @param subcommands - The subcommands to be performed on the binary value of the string at `key`, which could be + * any of the following: + * + * - {@link BitFieldGet} + * - {@link BitFieldSet} + * - {@link BitFieldIncrBy} + * - {@link BitFieldOverflow} + * + * @returns An array of results from the executed subcommands: + * + * - {@link BitFieldGet} returns the value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldSet} returns the old value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldIncrBy} returns the new value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldOverflow} determines the behavior of the {@link BitFieldSet} and {@link BitFieldIncrBy} + * subcommands when an overflow or underflow occurs. {@link BitFieldOverflow} does not return a value and + * does not contribute a value to the array response. + * + * @example + * ```typescript + * await client.set("key", "A"); // "A" has binary value 01000001 + * const result = await client.bitfield("key", [new BitFieldSet(new UnsignedEncoding(2), new BitOffset(1), 3), new BitFieldGet(new UnsignedEncoding(2), new BitOffset(1))]); + * console.log(result); // Output: [2, 3] - The old value at offset 1 with an unsigned encoding of 2 was 2. The new value at offset 1 with an unsigned encoding of 2 is 3. + * ``` + */ + public async bitfield( + key: string, + subcommands: BitFieldSubCommands[], + ): Promise<(number | null)[]> { + return this.createWritePromise(createBitField(key, subcommands)); + } + + /** + * Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + * + * See https://valkey.io/commands/bitfield_ro/ for more details. + * + * @param key - The key of the string. + * @param subcommands - The {@link BitFieldGet} subcommands to be performed. + * @returns An array of results from the {@link BitFieldGet} subcommands. + * + * since Valkey version 6.0.0. + * + * @example + * ```typescript + * await client.set("key", "A"); // "A" has binary value 01000001 + * const result = await client.bitfieldReadOnly("key", [new BitFieldGet(new UnsignedEncoding(2), new BitOffset(1))]); + * console.log(result); // Output: [2] - The value at offset 1 with an unsigned encoding of 2 is 2. + * ``` + */ + public async bitfieldReadOnly( + key: string, + subcommands: BitFieldGet[], + ): Promise { + return this.createWritePromise(createBitField(key, subcommands, true)); + } + /** Retrieve the value associated with `field` in the hash stored at `key`. * See https://valkey.io/commands/hget/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7361871411..df810a460b 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -494,6 +494,300 @@ export function createSetBit( ]); } +/** + * Represents a signed or unsigned argument encoding for the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + */ +export interface BitEncoding { + /** + * Returns the encoding as a string argument to be used in the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + * + * @returns The encoding as a string argument. + */ + toArg(): string; +} + +/** + * Represents a signed argument encoding. + */ +export class SignedEncoding implements BitEncoding { + private static readonly SIGNED_ENCODING_PREFIX = "i"; + private readonly encoding: string; + + /** + * Creates an instance of SignedEncoding. + * + * @param encodingLength - The bit size of the encoding. Must be less than 65 bits long. + */ + constructor(encodingLength: number) { + this.encoding = `${SignedEncoding.SIGNED_ENCODING_PREFIX}${encodingLength.toString()}`; + } + + public toArg(): string { + return this.encoding; + } +} + +/** + * Represents an unsigned argument encoding. + */ +export class UnsignedEncoding implements BitEncoding { + private static readonly UNSIGNED_ENCODING_PREFIX = "u"; + private readonly encoding: string; + + /** + * Creates an instance of UnsignedEncoding. + * + * @param encodingLength - The bit size of the encoding. Must be less than 64 bits long. + */ + constructor(encodingLength: number) { + this.encoding = `${UnsignedEncoding.UNSIGNED_ENCODING_PREFIX}${encodingLength.toString()}`; + } + + public toArg(): string { + return this.encoding; + } +} + +/** + * Represents an offset for an array of bits for the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + */ +export interface BitFieldOffset { + /** + * Returns the offset as a string argument to be used in the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + * + * @returns The offset as a string argument. + */ + toArg(): string; +} + +/** + * Represents an offset in an array of bits for the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + * + * For example, if we have the binary `01101001` with offset of 1 for an unsigned encoding of size 4, then the value + * is 13 from `0(1101)001`. + */ +export class BitOffset implements BitFieldOffset { + private readonly offset: string; + + /** + * Creates an instance of BitOffset. + * + * @param offset - The bit index offset in the array of bits. Must be greater than or equal to 0. + */ + constructor(offset: number) { + this.offset = offset.toString(); + } + + public toArg(): string { + return this.offset; + } +} + +/** + * Represents an offset in an array of bits for the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. The bit offset index is calculated as the numerical + * value of the offset multiplied by the encoding value. + * + * For example, if we have the binary 01101001 with offset multiplier of 1 for an unsigned encoding of size 4, then the + * value is 9 from `0110(1001)`. + */ +export class BitOffsetMultiplier implements BitFieldOffset { + private static readonly OFFSET_MULTIPLIER_PREFIX = "#"; + private readonly offset: string; + + /** + * Creates an instance of BitOffsetMultiplier. + * + * @param offset - The offset in the array of bits, which will be multiplied by the encoding value to get the final + * bit index offset. + */ + constructor(offset: number) { + this.offset = `${BitOffsetMultiplier.OFFSET_MULTIPLIER_PREFIX}${offset.toString()}`; + } + + public toArg(): string { + return this.offset; + } +} + +/** + * Represents subcommands for the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + */ +export interface BitFieldSubCommands { + /** + * Returns the subcommand as a list of string arguments to be used in the {@link BaseClient.bitfield|bitfield} or + * {@link BaseClient.bitfieldReadOnly|bitfieldReadOnly} commands. + * + * @returns The subcommand as a list of string arguments. + */ + toArgs(): string[]; +} + +/** + * Represents the "GET" subcommand for getting a value in the binary representation of the string stored in `key`. + */ +export class BitFieldGet implements BitFieldSubCommands { + private static readonly GET_COMMAND_STRING = "GET"; + private readonly encoding: BitEncoding; + private readonly offset: BitFieldOffset; + + /** + * Creates an instance of BitFieldGet. + * + * @param encoding - The bit encoding for the subcommand. + * @param offset - The offset in the array of bits from which to get the value. + */ + constructor(encoding: BitEncoding, offset: BitFieldOffset) { + this.encoding = encoding; + this.offset = offset; + } + + toArgs(): string[] { + return [ + BitFieldGet.GET_COMMAND_STRING, + this.encoding.toArg(), + this.offset.toArg(), + ]; + } +} + +/** + * Represents the "SET" subcommand for setting bits in the binary representation of the string stored in `key`. + */ +export class BitFieldSet implements BitFieldSubCommands { + private static readonly SET_COMMAND_STRING = "SET"; + private readonly encoding: BitEncoding; + private readonly offset: BitFieldOffset; + private readonly value: number; + + /** + * Creates an instance of BitFieldSet + * + * @param encoding - The bit encoding for the subcommand. + * @param offset - The offset in the array of bits where the value will be set. + * @param value - The value to set the bits in the binary value to. + */ + constructor(encoding: BitEncoding, offset: BitFieldOffset, value: number) { + this.encoding = encoding; + this.offset = offset; + this.value = value; + } + + toArgs(): string[] { + return [ + BitFieldSet.SET_COMMAND_STRING, + this.encoding.toArg(), + this.offset.toArg(), + this.value.toString(), + ]; + } +} + +/** + * Represents the "INCRBY" subcommand for increasing or decreasing bits in the binary representation of the string + * stored in `key`. + */ +export class BitFieldIncrBy implements BitFieldSubCommands { + private static readonly INCRBY_COMMAND_STRING = "INCRBY"; + private readonly encoding: BitEncoding; + private readonly offset: BitFieldOffset; + private readonly increment: number; + + /** + * Creates an instance of BitFieldIncrBy + * + * @param encoding - The bit encoding for the subcommand. + * @param offset - The offset in the array of bits where the value will be incremented. + * @param increment - The value to increment the bits in the binary value by. + */ + constructor( + encoding: BitEncoding, + offset: BitFieldOffset, + increment: number, + ) { + this.encoding = encoding; + this.offset = offset; + this.increment = increment; + } + + toArgs(): string[] { + return [ + BitFieldIncrBy.INCRBY_COMMAND_STRING, + this.encoding.toArg(), + this.offset.toArg(), + this.increment.toString(), + ]; + } +} + +/** + * Enumeration specifying bit overflow controls for the {@link BaseClient.bitfield|bitfield} command. + */ +export enum BitOverflowControl { + /** + * Performs modulo when overflows occur with unsigned encoding. When overflows occur with signed encoding, the value + * restarts at the most negative value. When underflows occur with signed encoding, the value restarts at the most + * positive value. + */ + WRAP = "WRAP", + /** + * Underflows remain set to the minimum value, and overflows remain set to the maximum value. + */ + SAT = "SAT", + /** + * Returns `None` when overflows occur. + */ + FAIL = "FAIL", +} + +/** + * Represents the "OVERFLOW" subcommand that determines the result of the "SET" or "INCRBY" + * {@link BaseClient.bitfield|bitfield} subcommands when an underflow or overflow occurs. + */ +export class BitFieldOverflow implements BitFieldSubCommands { + private static readonly OVERFLOW_COMMAND_STRING = "OVERFLOW"; + private readonly overflowControl: BitOverflowControl; + + /** + * Creates an instance of BitFieldOverflow. + * + * @param overflowControl - The desired overflow behavior. + */ + constructor(overflowControl: BitOverflowControl) { + this.overflowControl = overflowControl; + } + + toArgs(): string[] { + return [BitFieldOverflow.OVERFLOW_COMMAND_STRING, this.overflowControl]; + } +} + +/** + * @internal + */ +export function createBitField( + key: string, + subcommands: BitFieldSubCommands[], + readOnly: boolean = false, +): command_request.Command { + const requestType = readOnly + ? RequestType.BitFieldReadOnly + : RequestType.BitField; + let args: string[] = [key]; + + for (const subcommand of subcommands) { + args = args.concat(subcommand.toArgs()); + } + + return createCommand(requestType, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2cb2f6499b..2c7aa97dcf 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,13 @@ import { AggregationType, + BitFieldGet, + BitFieldIncrBy, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldOverflow, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldSet, // eslint-disable-line @typescript-eslint/no-unused-vars + BitFieldSubCommands, + BitOffset, // eslint-disable-line @typescript-eslint/no-unused-vars + BitOffsetMultiplier, // eslint-disable-line @typescript-eslint/no-unused-vars BitOffsetOptions, BitmapIndexType, BitwiseOperation, @@ -41,6 +48,7 @@ import { createBRPop, createBZMPop, createBitCount, + createBitField, createBitOp, createBitPos, createClientGetName, @@ -526,6 +534,50 @@ export class BaseTransaction> { return this.addAndReturn(createBitPos(key, bit, start, end, indexType)); } + /** + * Reads or modifies the array of bits representing the string that is held at `key` based on the specified + * `subcommands`. + * + * See https://valkey.io/commands/bitfield/ for more details. + * + * @param key - The key of the string. + * @param subcommands - The subcommands to be performed on the binary value of the string at `key`, which could be + * any of the following: + * + * - {@link BitFieldGet} + * - {@link BitFieldSet} + * - {@link BitFieldIncrBy} + * - {@link BitFieldOverflow} + * + * Command Response - An array of results from the executed subcommands: + * + * - {@link BitFieldGet} returns the value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldSet} returns the old value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldIncrBy} returns the new value in {@link BitOffset} or {@link BitOffsetMultiplier}. + * - {@link BitFieldOverflow} determines the behavior of the {@link BitFieldSet} and {@link BitFieldIncrBy} + * subcommands when an overflow or underflow occurs. {@link BitFieldOverflow} does not return a value and + * does not contribute a value to the array response. + */ + public bitfield(key: string, subcommands: BitFieldSubCommands[]): T { + return this.addAndReturn(createBitField(key, subcommands)); + } + + /** + * Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + * + * See https://valkey.io/commands/bitfield_ro/ for more details. + * + * @param key - The key of the string. + * @param subcommands - The {@link BitFieldGet} subcommands to be performed. + * + * Command Response - An array of results from the {@link BitFieldGet} subcommands. + * + * since Valkey version 6.0.0. + */ + public bitfieldReadOnly(key: string, subcommands: BitFieldGet[]): T { + return this.addAndReturn(createBitField(key, subcommands, true)); + } + /** Reads the configuration parameters of a running Redis server. * See https://valkey.io/commands/config-get/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index dc05269622..1cd668f6bb 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,13 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BitFieldGet, + BitFieldIncrBy, + BitFieldOverflow, + BitFieldSet, + BitOffset, + BitOffsetMultiplier, + BitOverflowControl, BitmapIndexType, BitwiseOperation, ClosingError, @@ -27,7 +34,9 @@ import { RequestError, ScoreFilter, Script, + SignedEncoding, SortOrder, + UnsignedEncoding, UpdateByScore, parseInfoResponse, } from "../"; @@ -781,6 +790,284 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitfield test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; + const foobar = "foobar"; + const u2 = new UnsignedEncoding(2); + const u7 = new UnsignedEncoding(7); + const i3 = new SignedEncoding(3); + const i8 = new SignedEncoding(8); + const offset1 = new BitOffset(1); + const offset5 = new BitOffset(5); + const offset_multiplier4 = new BitOffsetMultiplier(4); + const offset_multiplier8 = new BitOffsetMultiplier(8); + const overflowSet = new BitFieldSet(u2, offset1, -10); + const overflowGet = new BitFieldGet(u2, offset1); + + // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + checkSimple(await client.set(key1, foobar)).toEqual("OK"); + + // SET tests + expect( + await client.bitfield(key1, [ + // binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(u2, offset1, 2), + // binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(i3, offset5, 3), + // binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + new BitFieldSet(u7, offset_multiplier4, 18), + // addressing with SET or INCRBY bits outside the current string length will enlarge the string, + // zero-padding it, as needed, for the minimal length needed, according to the most far bit touched. + // + // binary value becomes: + // 01000011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 (00010100) + new BitFieldSet(i8, offset_multiplier8, 20), + new BitFieldGet(u2, offset1), + new BitFieldGet(i3, offset5), + new BitFieldGet(u7, offset_multiplier4), + new BitFieldGet(i8, offset_multiplier8), + ]), + ).toEqual([3, -2, 19, 0, 2, 3, 18, 20]); + + // INCRBY tests + expect( + await client.bitfield(key1, [ + // binary value becomes: + // 0(11)00011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + new BitFieldIncrBy(u2, offset1, 1), + // binary value becomes: + // 01100(101) 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + new BitFieldIncrBy(i3, offset5, 2), + // binary value becomes: + // 01100101 01101111 01101111 0110(0001 111)00001 01110010 00000000 00000000 00010100 + new BitFieldIncrBy(u7, offset_multiplier4, -3), + // binary value becomes: + // 01100101 01101111 01101111 01100001 11100001 01110010 00000000 00000000 (00011110) + new BitFieldIncrBy(i8, offset_multiplier8, 10), + ]), + ).toEqual([3, -3, 15, 30]); + + // OVERFLOW WRAP is used by default if no OVERFLOW is specified + expect( + await client.bitfield(key2, [ + overflowSet, + new BitFieldOverflow(BitOverflowControl.WRAP), + overflowSet, + overflowGet, + ]), + ).toEqual([0, 2, 2]); + + // OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + expect( + await client.bitfield(key2, [ + overflowSet, + new BitFieldOverflow(BitOverflowControl.SAT), + overflowSet, + overflowGet, + new BitFieldOverflow(BitOverflowControl.FAIL), + overflowSet, + ]), + ).toEqual([2, 2, 3, null]); + + // if the key doesn't exist, the operation is performed as though the missing value was a string with all bits + // set to 0. + expect( + await client.bitfield(nonExistingKey, [ + new BitFieldSet( + new UnsignedEncoding(2), + new BitOffset(3), + 2, + ), + ]), + ).toEqual([0]); + + // empty subcommands argument returns an empty list + expect(await client.bitfield(key1, [])).toEqual([]); + + // invalid argument - offset must be >= 0 + await expect( + client.bitfield(key1, [ + new BitFieldSet( + new UnsignedEncoding(5), + new BitOffset(-1), + 1, + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - encoding size must be > 0 + await expect( + client.bitfield(key1, [ + new BitFieldSet( + new UnsignedEncoding(0), + new BitOffset(1), + 1, + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - unsigned encoding must be < 64 + await expect( + client.bitfield(key1, [ + new BitFieldSet( + new UnsignedEncoding(64), + new BitOffset(1), + 1, + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - signed encoding must be < 65 + await expect( + client.bitfield(key1, [ + new BitFieldSet( + new SignedEncoding(65), + new BitOffset(1), + 1, + ), + ]), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a string + expect(await client.sadd(setKey, [foobar])).toEqual(1); + await expect( + client.bitfield(setKey, [ + new BitFieldSet( + new SignedEncoding(3), + new BitOffset(1), + 2, + ), + ]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitfieldReadOnly test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.0.0")) { + return; + } + + const key = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; + const foobar = "foobar"; + const unsignedOffsetGet = new BitFieldGet( + new UnsignedEncoding(2), + new BitOffset(1), + ); + + // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + checkSimple(await client.set(key, foobar)).toEqual("OK"); + expect( + await client.bitfieldReadOnly(key, [ + // Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsignedOffsetGet, + // Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + new BitFieldGet( + new SignedEncoding(3), + new BitOffset(5), + ), + // Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + new BitFieldGet( + new UnsignedEncoding(7), + new BitOffsetMultiplier(3), + ), + // Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + new BitFieldGet( + new SignedEncoding(8), + new BitOffsetMultiplier(2), + ), + ]), + ).toEqual([3, -2, 118, 111]); + + // offset is greater than current length of string: the operation is performed like the missing part all + // consists of bits set to 0. + expect( + await client.bitfieldReadOnly(key, [ + new BitFieldGet( + new UnsignedEncoding(3), + new BitOffset(100), + ), + ]), + ).toEqual([0]); + + // similarly, if the key doesn't exist, the operation is performed as though the missing value was a string with + // all bits set to 0. + expect( + await client.bitfieldReadOnly(nonExistingKey, [ + unsignedOffsetGet, + ]), + ).toEqual([0]); + + // empty subcommands argument returns an empty list + expect(await client.bitfieldReadOnly(key, [])).toEqual([]); + + // invalid argument - offset must be >= 0 + await expect( + client.bitfieldReadOnly(key, [ + new BitFieldGet( + new UnsignedEncoding(5), + new BitOffset(-1), + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - encoding size must be > 0 + await expect( + client.bitfieldReadOnly(key, [ + new BitFieldGet( + new UnsignedEncoding(0), + new BitOffset(1), + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - unsigned encoding must be < 64 + await expect( + client.bitfieldReadOnly(key, [ + new BitFieldGet( + new UnsignedEncoding(64), + new BitOffset(1), + ), + ]), + ).rejects.toThrow(RequestError); + + // invalid argument - signed encoding must be < 65 + await expect( + client.bitfieldReadOnly(key, [ + new BitFieldGet( + new SignedEncoding(65), + new BitOffset(1), + ), + ]), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a string + expect(await client.sadd(setKey, [foobar])).toEqual(1); + await expect( + client.bitfieldReadOnly(setKey, [ + new BitFieldGet( + new SignedEncoding(3), + new BitOffset(1), + ), + ]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set with timeout parameter_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2db39cb409..3e60effd72 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,10 @@ import { v4 as uuidv4 } from "uuid"; import { BaseClient, BaseClientConfiguration, + BitFieldGet, + BitFieldSet, + BitOffset, + BitOffsetMultiplier, BitmapIndexType, BitwiseOperation, ClusterTransaction, @@ -24,8 +28,10 @@ import { ProtocolVersion, ReturnType, ScoreFilter, + SignedEncoding, SortOrder, Transaction, + UnsignedEncoding, } from ".."; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -804,6 +810,16 @@ export async function transactionTest( baseTransaction.bitpos(key17, 1); responseData.push(["bitpos(key17, 1)", 1]); + if (gte("6.0.0", version)) { + baseTransaction.bitfieldReadOnly(key17, [ + new BitFieldGet(new SignedEncoding(5), new BitOffset(3)), + ]); + responseData.push([ + "bitfieldReadOnly(key17, [new BitFieldGet(...)])", + [6], + ]); + } + baseTransaction.set(key19, "abcdef"); responseData.push(['set(key19, "abcdef")', "OK"]); baseTransaction.bitop(BitwiseOperation.AND, key19, [key19, key17]); @@ -831,6 +847,15 @@ export async function transactionTest( ]); } + baseTransaction.bitfield(key17, [ + new BitFieldSet( + new UnsignedEncoding(10), + new BitOffsetMultiplier(3), + 4, + ), + ]); + responseData.push(["bitfield(key17, [new BitFieldSet(...)])", [609]]); + baseTransaction.pfadd(key11, ["a", "b", "c"]); responseData.push(['pfadd(key11, ["a", "b", "c"])', 1]); baseTransaction.pfcount([key11]); diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 35d02a7eff..5be9749d65 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -5779,7 +5779,7 @@ async def bitfield_read_only( Examples: >>> await client.set("my_key", "A") # "A" has binary value 01000001 >>> await client.bitfield_read_only("my_key", [BitFieldGet(UnsignedEncoding(2), Offset(1))]) - [2] # The value at offset 1 with an unsigned encoding of 2 is 3. + [2] # The value at offset 1 with an unsigned encoding of 2 is 2. Since: Valkey version 6.0.0. """ From 15db0e89c586d55660bc015e65657a6fa211ff3d Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:35:11 -0700 Subject: [PATCH 093/236] Python: minor corrections in BITFIELD and BITFIELD_RO docs (#2048) Signed-off-by: aaron-congo --- CHANGELOG.md | 1 + python/python/glide/async_commands/core.py | 8 ++++---- python/python/glide/async_commands/transaction.py | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ea272755..a8f550821e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ #### Changes * Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023)) * Java, Python: Update docs for GEOSEARCH command ([#2017](https://github.com/valkey-io/valkey-glide/pull/2017)) +* Python: Update docs for BITFIELD and BITFIELD_RO commands ([#2048](https://github.com/valkey-io/valkey-glide/pull/2048)) * Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019)) * Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007)) * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 5be9749d65..e20f07bbc2 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -5743,16 +5743,16 @@ async def bitfield( Returns: List[Optional[int]]: An array of results from the executed subcommands: - - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. - - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. - - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldGet` returns the value in `BitOffset` or `BitOffsetMultiplier`. + - `BitFieldSet` returns the old value in `BitOffset` or `BitOffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `BitOffset` or `BitOffsetMultiplier`. - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list response. Examples: >>> await client.set("my_key", "A") # "A" has binary value 01000001 - >>> await client.bitfield("my_key", [BitFieldSet(UnsignedEncoding(2), Offset(1), 3), BitFieldGet(UnsignedEncoding(2), Offset(1))]) + >>> await client.bitfield("my_key", [BitFieldSet(UnsignedEncoding(2), BitOffset(1), 3), BitFieldGet(UnsignedEncoding(2), BitOffset(1))]) [2, 3] # The old value at offset 1 with an unsigned encoding of 2 was 2. The new value at offset 1 with an unsigned encoding of 2 is 3. """ args = [key] + _create_bitfield_args(subcommands) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 6167b549c1..6a95c2f081 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -4196,9 +4196,9 @@ def bitfield( Command response: List[Optional[int]]: An array of results from the executed subcommands: - - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. - - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. - - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldGet` returns the value in `BitOffset` or `BitOffsetMultiplier`. + - `BitFieldSet` returns the old value in `BitOffset` or `BitOffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `BitOffset` or `BitOffsetMultiplier`. - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list response. From 1d50b4b7efa691c218f0730a4d33bdfdeb24935d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 30 Jul 2024 10:00:14 -0700 Subject: [PATCH 094/236] address new comments Signed-off-by: Chloe Yip --- node/src/Transaction.ts | 3 +-- node/tests/SharedTests.ts | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3cfd600c1c..501221fce4 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -354,9 +354,8 @@ export class BaseTransaction> { * * See https://valkey.io/commands/msetnx/ for more details. * - * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. * @param keyValueMap - A key-value map consisting of keys and their respective values to set. - * @returns `true` if all keys were set. `false` if no key was set. + * Command Response - `true` if all keys were set. `false` if no key was set. */ public msetnx(keyValueMap: Record): T { return this.addAndReturn(createMSetNX(keyValueMap)); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 48338ada3e..664bf1de6e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -344,6 +344,12 @@ export function runBaseTests(config: { expect(await client.get(key3)).toEqual(null); checkSimple(await client.get(key2)).toEqual(value); + + // empty map and RequestError is thrown + const emptyMap = {}; + await expect(client.msetnx(emptyMap)).rejects.toThrow( + RequestError, + ); }, protocol); }, config.timeout, From 4f874356b56f6de1e92a3d7f3f510543391965ca Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 30 Jul 2024 11:59:09 -0700 Subject: [PATCH 095/236] add blocking test Signed-off-by: Chloe Yip --- node/tests/RedisClient.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index fb4b3e4bd8..e56abfd09b 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -30,6 +30,7 @@ import { transactionTest, validateTransactionResponse, } from "./TestUtilities"; +import { ListDirection } from ".."; /* eslint-disable @typescript-eslint/no-var-requires */ @@ -126,6 +127,38 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "blocking timeout tests_%p", + async (protocol) => { + client = await GlideClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + 300, + ), + ); + + const blmovePromise = client.blmove( + "source", + "destination", + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ); + const timeoutPromise = new Promise((resolve) => { + setTimeout(resolve, 500); + }); + + try { + await Promise.race([blmovePromise, timeoutPromise]); + } finally { + Promise.resolve(blmovePromise); + client.close(); + } + }, + 5000, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "select dbsize flushdb test %p", async (protocol) => { From d24f0ad385b808edc42a2a27ad3bbebeb60c8b91 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 30 Jul 2024 13:13:30 -0700 Subject: [PATCH 096/236] change name of blocking test Signed-off-by: Chloe Yip --- node/tests/RedisClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index e56abfd09b..4105876521 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -128,7 +128,7 @@ describe("GlideClient", () => { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "blocking timeout tests_%p", + "check that blocking commands returns never timeout_%p", async (protocol) => { client = await GlideClient.createClient( getClientConfigurationOption( From 9a7df3822a7e9825e39a6742700d6411ccde500c Mon Sep 17 00:00:00 2001 From: sullis Date: Tue, 30 Jul 2024 13:19:39 -0700 Subject: [PATCH 097/236] Java: Use Lombok ToString (#2030) Signed-off-by: sullis --- .../java/glide/api/models/configuration/BackoffStrategy.java | 2 ++ .../api/models/configuration/GlideClientConfiguration.java | 2 ++ .../main/java/glide/api/models/configuration/NodeAddress.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java b/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java index c2e8d0f0c1..bd95629866 100644 --- a/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java +++ b/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NonNull; +import lombok.ToString; /** * Represents the strategy used to determine how and when to reconnect, in case of connection @@ -24,6 +25,7 @@ */ @Getter @Builder +@ToString public class BackoffStrategy { /** * Number of retry attempts that the client should perform when disconnected from the server, diff --git a/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java index 5c0f04c945..edb7bbb326 100644 --- a/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java @@ -3,6 +3,7 @@ import glide.api.GlideClient; import lombok.Getter; +import lombok.ToString; import lombok.experimental.SuperBuilder; /** @@ -27,6 +28,7 @@ */ @Getter @SuperBuilder +@ToString public class GlideClientConfiguration extends BaseClientConfiguration { /** Strategy used to determine how and when to reconnect, in case of connection failures. */ private final BackoffStrategy reconnectStrategy; diff --git a/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java b/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java index b3766bad3b..b59ae39f7c 100644 --- a/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java +++ b/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NonNull; +import lombok.ToString; /** * Represents the address and port of a node in the cluster or in standalone installation. @@ -17,6 +18,7 @@ */ @Getter @Builder +@ToString public class NodeAddress { public static final String DEFAULT_HOST = "localhost"; public static final Integer DEFAULT_PORT = 6379; From 8a6a383871a0ddae42b59a2b88965fe1bb71e3d5 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Tue, 30 Jul 2024 13:53:06 -0700 Subject: [PATCH 098/236] Node: add minimal binary support (#2047) * Added minimal binary support for Node based on https://github.com/valkey-io/valkey-glide/pull/1953 Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- node/rust-client/src/lib.rs | 66 ++- node/src/BaseClient.ts | 10 +- node/tests/RedisClient.test.ts | 47 +- node/tests/RedisClusterClient.test.ts | 52 +- node/tests/SharedTests.ts | 713 ++++++++++++-------------- node/tests/TestUtilities.ts | 6 +- 6 files changed, 438 insertions(+), 456 deletions(-) diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index b5ea8f39c2..c301e082de 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -160,19 +160,37 @@ pub fn init(level: Option, file_name: Option<&str>) -> Level { logger_level.into() } -fn redis_value_to_js(val: Value, js_env: Env) -> Result { +fn redis_value_to_js(val: Value, js_env: Env, string_decoder: bool) -> Result { match val { Value::Nil => js_env.get_null().map(|val| val.into_unknown()), - Value::SimpleString(str) => Ok(js_env - .create_buffer_with_data(str.as_bytes().to_vec())? - .into_unknown()), + Value::SimpleString(str) => { + if string_decoder { + Ok(js_env + .create_string_from_std(str) + .map(|val| val.into_unknown())?) + } else { + Ok(js_env + .create_buffer_with_data(str.as_bytes().to_vec())? + .into_unknown()) + } + } Value::Okay => js_env.create_string("OK").map(|val| val.into_unknown()), Value::Int(num) => js_env.create_int64(num).map(|val| val.into_unknown()), - Value::BulkString(data) => Ok(js_env.create_buffer_with_data(data)?.into_unknown()), + Value::BulkString(data) => { + if string_decoder { + let str = to_js_result(std::str::from_utf8(data.as_ref()))?; + Ok(js_env.create_string(str).map(|val| val.into_unknown())?) + } else { + Ok(js_env.create_buffer_with_data(data)?.into_unknown()) + } + } Value::Array(array) => { let mut js_array_view = js_env.create_array_with_length(array.len())?; for (index, item) in array.into_iter().enumerate() { - js_array_view.set_element(index as u32, redis_value_to_js(item, js_env)?)?; + js_array_view.set_element( + index as u32, + redis_value_to_js(item, js_env, string_decoder)?, + )?; } Ok(js_array_view.into_unknown()) } @@ -180,7 +198,7 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { let mut obj = js_env.create_object()?; for (key, value) in map { let field_name = String::from_owned_redis_value(key).map_err(to_js_error)?; - let value = redis_value_to_js(value, js_env)?; + let value = redis_value_to_js(value, js_env, string_decoder)?; obj.set_named_property(&field_name, value)?; } Ok(obj.into_unknown()) @@ -192,10 +210,16 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { // "type and the String type, and return a string in both cases."" // https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md Value::VerbatimString { format: _, text } => { - // VerbatimString is binary safe -> convert it into such - Ok(js_env - .create_buffer_with_data(text.as_bytes().to_vec())? - .into_unknown()) + if string_decoder { + Ok(js_env + .create_string_from_std(text) + .map(|val| val.into_unknown())?) + } else { + // VerbatimString is binary safe -> convert it into such + Ok(js_env + .create_buffer_with_data(text.as_bytes().to_vec())? + .into_unknown()) + } } Value::BigNumber(num) => { let sign = num.is_negative(); @@ -208,16 +232,19 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { // TODO - return a set object instead of an array object let mut js_array_view = js_env.create_array_with_length(array.len())?; for (index, item) in array.into_iter().enumerate() { - js_array_view.set_element(index as u32, redis_value_to_js(item, js_env)?)?; + js_array_view.set_element( + index as u32, + redis_value_to_js(item, js_env, string_decoder)?, + )?; } Ok(js_array_view.into_unknown()) } Value::Attribute { data, attributes } => { let mut obj = js_env.create_object()?; - let value = redis_value_to_js(*data, js_env)?; + let value = redis_value_to_js(*data, js_env, string_decoder)?; obj.set_named_property("value", value)?; - let value = redis_value_to_js(Value::Map(attributes), js_env)?; + let value = redis_value_to_js(Value::Map(attributes), js_env, string_decoder)?; obj.set_named_property("attributes", value)?; Ok(obj.into_unknown()) @@ -227,7 +254,7 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { obj.set_named_property("kind", format!("{kind:?}"))?; let js_array_view = data .into_iter() - .map(|item| redis_value_to_js(item, js_env)) + .map(|item| redis_value_to_js(item, js_env, string_decoder)) .collect::, _>>()?; obj.set_named_property("values", js_array_view)?; Ok(obj.into_unknown()) @@ -238,7 +265,12 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { #[napi( ts_return_type = "null | string | Uint8Array | number | {} | Boolean | BigInt | Set | any[]" )] -pub fn value_from_split_pointer(js_env: Env, high_bits: u32, low_bits: u32) -> Result { +pub fn value_from_split_pointer( + js_env: Env, + high_bits: u32, + low_bits: u32, + string_decoder: bool, +) -> Result { let mut bytes = [0_u8; 8]; (&mut bytes[..4]) .write_u32::(low_bits) @@ -248,7 +280,7 @@ pub fn value_from_split_pointer(js_env: Env, high_bits: u32, low_bits: u32) -> R .unwrap(); let pointer = u64::from_le_bytes(bytes); let value = unsafe { Box::from_raw(pointer as *mut Value) }; - redis_value_to_js(*value, js_env) + redis_value_to_js(*value, js_env, string_decoder) } // Pointers are split because JS cannot represent a full usize using its `number` object. diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 01275730eb..a0d6d7047b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -442,9 +442,11 @@ export class BaseClient { const pointer = message.respPointer; if (typeof pointer === "number") { - resolve(valueFromSplitPointer(0, pointer)); + // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 + resolve(valueFromSplitPointer(0, pointer, true)); } else { - resolve(valueFromSplitPointer(pointer.high, pointer.low)); + // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 + resolve(valueFromSplitPointer(pointer.high, pointer.low, true)); } } else if (message.constantResponse === response.ConstantResponse.OK) { resolve("OK"); @@ -708,11 +710,15 @@ export class BaseClient { nextPushNotificationValue = valueFromSplitPointer( responsePointer.high, responsePointer.low, + // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 + true, ) as Record; } else { nextPushNotificationValue = valueFromSplitPointer( 0, responsePointer, + // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 + true, ) as Record; } diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 8f7fbc1c8f..ba728371ef 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -19,7 +19,6 @@ import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { checkFunctionListResponse, - checkSimple, convertStringArrayToBuffer, flushAndCloseClient, generateLuaLibCode, @@ -165,23 +164,23 @@ describe("GlideClient", () => { client = await GlideClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), ); - checkSimple(await client.select(0)).toEqual("OK"); + expect(await client.select(0)).toEqual("OK"); const key = uuidv4(); const value = uuidv4(); const result = await client.set(key, value); - checkSimple(result).toEqual("OK"); + expect(result).toEqual("OK"); - checkSimple(await client.select(1)).toEqual("OK"); + expect(await client.select(1)).toEqual("OK"); expect(await client.get(key)).toEqual(null); - checkSimple(await client.flushdb()).toEqual("OK"); + expect(await client.flushdb()).toEqual("OK"); expect(await client.dbsize()).toEqual(0); - checkSimple(await client.select(0)).toEqual("OK"); - checkSimple(await client.get(key)).toEqual(value); + expect(await client.select(0)).toEqual("OK"); + expect(await client.get(key)).toEqual(value); expect(await client.dbsize()).toBeGreaterThan(0); - checkSimple(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); expect(await client.dbsize()).toEqual(0); }, ); @@ -433,7 +432,7 @@ describe("GlideClient", () => { }), ).toEqual(true); expect(await client.select(index1)).toEqual("OK"); - checkSimple(await client.get(destination)).toEqual(value1); + expect(await client.get(destination)).toEqual(value1); // new value for source key expect(await client.select(index0)).toEqual("OK"); @@ -455,9 +454,9 @@ describe("GlideClient", () => { // new value only gets copied to DB 2 expect(await client.select(index1)).toEqual("OK"); - checkSimple(await client.get(destination)).toEqual(value1); + expect(await client.get(destination)).toEqual(value1); expect(await client.select(index2)).toEqual("OK"); - checkSimple(await client.get(destination)).toEqual(value2); + expect(await client.get(destination)).toEqual(value2); // both exists, with REPLACE, when value isn't the same, source always get copied to // destination @@ -469,7 +468,7 @@ describe("GlideClient", () => { }), ).toEqual(true); expect(await client.select(index1)).toEqual("OK"); - checkSimple(await client.get(destination)).toEqual(value2); + expect(await client.get(destination)).toEqual(value2); //transaction tests const transaction = new Transaction(); @@ -482,7 +481,7 @@ describe("GlideClient", () => { transaction.get(destination); const results = await client.exec(transaction); - checkSimple(results).toEqual(["OK", "OK", true, value1]); + expect(results).toEqual(["OK", "OK", true, value1]); client.close(); }, @@ -508,12 +507,12 @@ describe("GlideClient", () => { ); expect(await client.functionList()).toEqual([]); - checkSimple(await client.functionLoad(code)).toEqual(libName); + expect(await client.functionLoad(code)).toEqual(libName); - checkSimple( + expect( await client.fcall(funcName, [], ["one", "two"]), ).toEqual("one"); - checkSimple( + expect( await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); @@ -541,9 +540,7 @@ describe("GlideClient", () => { ); // re-load library with replace - checkSimple(await client.functionLoad(code, true)).toEqual( - libName, - ); + expect(await client.functionLoad(code, true)).toEqual(libName); // overwrite lib with new code const func2Name = "myfunc2c" + uuidv4().replaceAll("-", ""); @@ -555,7 +552,7 @@ describe("GlideClient", () => { ]), true, ); - checkSimple(await client.functionLoad(newCode, true)).toEqual( + expect(await client.functionLoad(newCode, true)).toEqual( libName, ); @@ -577,10 +574,10 @@ describe("GlideClient", () => { newCode, ); - checkSimple( + expect( await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); - checkSimple( + expect( await client.fcallReadonly(func2Name, [], ["one", "two"]), ).toEqual(2); } finally { @@ -611,7 +608,7 @@ describe("GlideClient", () => { // verify function does not yet exist expect(await client.functionList()).toEqual([]); - checkSimple(await client.functionLoad(code)).toEqual(libName); + expect(await client.functionLoad(code)).toEqual(libName); // Flush functions expect(await client.functionFlush(FlushMode.SYNC)).toEqual( @@ -625,7 +622,7 @@ describe("GlideClient", () => { expect(await client.functionList()).toEqual([]); // Attempt to re-load library without overwriting to ensure FLUSH was effective - checkSimple(await client.functionLoad(code)).toEqual(libName); + expect(await client.functionLoad(code)).toEqual(libName); } finally { expect(await client.functionFlush()).toEqual("OK"); client.close(); @@ -653,7 +650,7 @@ describe("GlideClient", () => { // verify function does not yet exist expect(await client.functionList()).toEqual([]); - checkSimple(await client.functionLoad(code)).toEqual(libName); + expect(await client.functionLoad(code)).toEqual(libName); // Delete the function expect(await client.functionDelete(libName)).toEqual("OK"); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 554763a3fd..d70ef939df 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -2,14 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { - afterAll, - afterEach, - beforeAll, - describe, - expect, - it, -} from "@jest/globals"; +import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; import { @@ -29,7 +22,6 @@ import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, checkFunctionListResponse, - checkSimple, flushAndCloseClient, generateLuaLibCode, getClientConfigurationOption, @@ -569,7 +561,7 @@ describe("GlideClusterClient", () => { // source exists, destination does not expect(await client.set(source, value1)).toEqual("OK"); expect(await client.copy(source, destination, false)).toEqual(true); - checkSimple(await client.get(destination)).toEqual(value1); + expect(await client.get(destination)).toEqual(value1); // new value for source key expect(await client.set(source, value2)).toEqual("OK"); @@ -579,11 +571,11 @@ describe("GlideClusterClient", () => { expect(await client.copy(source, destination, false)).toEqual( false, ); - checkSimple(await client.get(destination)).toEqual(value1); + expect(await client.get(destination)).toEqual(value1); // both exists, with REPLACE expect(await client.copy(source, destination, true)).toEqual(true); - checkSimple(await client.get(destination)).toEqual(value2); + expect(await client.get(destination)).toEqual(value2); //transaction tests const transaction = new ClusterTransaction(); @@ -592,7 +584,7 @@ describe("GlideClusterClient", () => { transaction.get(destination); const results = await client.exec(transaction); - checkSimple(results).toEqual(["OK", true, value1]); + expect(results).toEqual(["OK", true, value1]); client.close(); }, @@ -606,20 +598,20 @@ describe("GlideClusterClient", () => { ); expect(await client.dbsize()).toBeGreaterThanOrEqual(0); - checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); expect(await client.dbsize()).toBeGreaterThan(0); - checkSimple(await client.flushall()).toEqual("OK"); + expect(await client.flushall()).toEqual("OK"); expect(await client.dbsize()).toEqual(0); - checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); expect(await client.dbsize()).toEqual(1); - checkSimple(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); + expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); expect(await client.dbsize()).toEqual(0); - checkSimple(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); expect(await client.dbsize()).toEqual(1); - checkSimple(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); expect(await client.dbsize()).toEqual(0); client.close(); @@ -670,9 +662,9 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual([]), ); // load the library - checkSimple( - await client.functionLoad(code), - ).toEqual(libName); + expect(await client.functionLoad(code)).toEqual( + libName, + ); functionList = await client.functionList( { libNamePattern: libName }, @@ -707,8 +699,7 @@ describe("GlideClusterClient", () => { checkClusterResponse( fcall as object, singleNodeRoute, - (value) => - checkSimple(value).toEqual("one"), + (value) => expect(value).toEqual("one"), ); fcall = await client.fcallReadonlyWithRoute( funcName, @@ -718,8 +709,7 @@ describe("GlideClusterClient", () => { checkClusterResponse( fcall as object, singleNodeRoute, - (value) => - checkSimple(value).toEqual("one"), + (value) => expect(value).toEqual("one"), ); // re-load library without replace @@ -730,7 +720,7 @@ describe("GlideClusterClient", () => { ); // re-load library with replace - checkSimple( + expect( await client.functionLoad(code, true), ).toEqual(libName); @@ -745,7 +735,7 @@ describe("GlideClusterClient", () => { ]), true, ); - checkSimple( + expect( await client.functionLoad(newCode, true), ).toEqual(libName); @@ -858,7 +848,7 @@ describe("GlideClusterClient", () => { ); // load the library - checkSimple( + expect( await client.functionLoad( code, undefined, @@ -889,7 +879,7 @@ describe("GlideClusterClient", () => { ); // Attempt to re-load library without overwriting to ensure FLUSH was effective - checkSimple( + expect( await client.functionLoad( code, undefined, @@ -953,7 +943,7 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual([]), ); // load the library - checkSimple( + expect( await client.functionLoad( code, undefined, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d016301b61..c0a36757ea 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -45,10 +45,8 @@ import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { Client, GetAndSetRandomValue, - checkSimple, compareMaps, getFirstResult, - intoArray, intoString, } from "./TestUtilities"; @@ -100,8 +98,8 @@ export function runBaseTests(config: { } const result = await client.customCommand(["CLIENT", "INFO"]); - expect(intoString(result)).toContain("lib-name=GlideJS"); - expect(intoString(result)).toContain("lib-ver=unknown"); + expect(result).toContain("lib-name=GlideJS"); + expect(result).toContain("lib-ver=unknown"); }, protocol); }, config.timeout, @@ -156,9 +154,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest( async (client: BaseClient) => { - expect(intoString(await client.clientGetName())).toBe( - "TEST_CLIENT", - ); + expect(await client.clientGetName()).toBe("TEST_CLIENT"); }, protocol, "TEST_CLIENT", @@ -179,9 +175,9 @@ export function runBaseTests(config: { key, value, ]); - checkSimple(setResult).toEqual("OK"); + expect(setResult).toEqual("OK"); const result = await client.customCommand(["GET", key]); - checkSimple(result).toEqual(value); + expect(result).toEqual(value); }, protocol); }, config.timeout, @@ -202,20 +198,20 @@ export function runBaseTests(config: { key1, value1, ]); - checkSimple(setResult1).toEqual("OK"); + expect(setResult1).toEqual("OK"); const setResult2 = await client.customCommand([ "SET", key2, value2, ]); - checkSimple(setResult2).toEqual("OK"); + expect(setResult2).toEqual("OK"); const mget_result = await client.customCommand([ "MGET", key1, key2, key3, ]); - checkSimple(mget_result).toEqual([value1, value2, null]); + expect(mget_result).toEqual([value1, value2, null]); }, protocol); }, config.timeout, @@ -258,15 +254,13 @@ export function runBaseTests(config: { `test config rewrite_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const serverInfo = intoString( - await client.info([InfoOptions.Server]), - ); + const serverInfo = await client.info([InfoOptions.Server]); const conf_file = parseInfoResponse( getFirstResult(serverInfo).toString(), )["config_file"]; if (conf_file.length > 0) { - checkSimple(await client.configRewrite()).toEqual("OK"); + expect(await client.configRewrite()).toEqual("OK"); } else { try { /// We expect Redis to return an error since the test cluster doesn't use redis.conf file @@ -292,11 +286,9 @@ export function runBaseTests(config: { const oldResult = await client.info([InfoOptions.Commandstats]); const oldResultAsString = intoString(oldResult); expect(oldResultAsString).toContain("cmdstat_set"); - checkSimple(await client.configResetStat()).toEqual("OK"); + expect(await client.configResetStat()).toEqual("OK"); - const result = intoArray( - await client.info([InfoOptions.Commandstats]), - ); + const result = await client.info([InfoOptions.Commandstats]); expect(result).not.toContain("cmdstat_set"); }, protocol); }, @@ -316,8 +308,8 @@ export function runBaseTests(config: { [key2]: value, [key3]: value, }; - checkSimple(await client.mset(keyValueList)).toEqual("OK"); - checkSimple( + expect(await client.mset(keyValueList)).toEqual("OK"); + expect( await client.mget([key1, key2, "nonExistingKey", key3]), ).toEqual([value, value, null, value]); }, protocol); @@ -330,13 +322,13 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "10")).toEqual("OK"); + expect(await client.set(key, "10")).toEqual("OK"); expect(await client.incr(key)).toEqual(11); - checkSimple(await client.get(key)).toEqual("11"); - checkSimple(await client.incrBy(key, 4)).toEqual(15); - checkSimple(await client.get(key)).toEqual("15"); - checkSimple(await client.incrByFloat(key, 1.5)).toEqual(16.5); - checkSimple(await client.get(key)).toEqual("16.5"); + expect(await client.get(key)).toEqual("11"); + expect(await client.incrBy(key, 4)).toEqual(15); + expect(await client.get(key)).toEqual("15"); + expect(await client.incrByFloat(key, 1.5)).toEqual(16.5); + expect(await client.get(key)).toEqual("16.5"); }, protocol); }, config.timeout, @@ -351,11 +343,11 @@ export function runBaseTests(config: { const key3 = uuidv4(); /// key1 and key2 does not exist, so it set to 0 before performing the operation. expect(await client.incr(key1)).toEqual(1); - checkSimple(await client.get(key1)).toEqual("1"); + expect(await client.get(key1)).toEqual("1"); expect(await client.incrBy(key2, 2)).toEqual(2); - checkSimple(await client.get(key2)).toEqual("2"); + expect(await client.get(key2)).toEqual("2"); expect(await client.incrByFloat(key3, -0.5)).toEqual(-0.5); - checkSimple(await client.get(key3)).toEqual("-0.5"); + expect(await client.get(key3)).toEqual("-0.5"); }, protocol); }, config.timeout, @@ -366,7 +358,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.incr(key)).toThrow(); @@ -400,8 +392,8 @@ export function runBaseTests(config: { `ping test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - checkSimple(await client.ping()).toEqual("PONG"); - checkSimple(await client.ping("Hello")).toEqual("Hello"); + expect(await client.ping()).toEqual("PONG"); + expect(await client.ping("Hello")).toEqual("Hello"); }, protocol); }, config.timeout, @@ -424,11 +416,11 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "10")).toEqual("OK"); + expect(await client.set(key, "10")).toEqual("OK"); expect(await client.decr(key)).toEqual(9); - checkSimple(await client.get(key)).toEqual("9"); + expect(await client.get(key)).toEqual("9"); expect(await client.decrBy(key, 4)).toEqual(5); - checkSimple(await client.get(key)).toEqual("5"); + expect(await client.get(key)).toEqual("5"); }, protocol); }, config.timeout, @@ -443,10 +435,10 @@ export function runBaseTests(config: { /// key1 and key2 does not exist, so it set to 0 before performing the operation. expect(await client.get(key1)).toBeNull(); expect(await client.decr(key1)).toEqual(-1); - checkSimple(await client.get(key1)).toEqual("-1"); + expect(await client.get(key1)).toEqual("-1"); expect(await client.get(key2)).toBeNull(); expect(await client.decrBy(key2, 3)).toEqual(-3); - checkSimple(await client.get(key2)).toEqual("-3"); + expect(await client.get(key2)).toEqual("-3"); }, protocol); }, config.timeout, @@ -485,6 +477,7 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const key1 = `{key}-${uuidv4()}`; const key2 = `{key}-${uuidv4()}`; + const key3 = `{key}-${uuidv4()}`; const keys = [key1, key2]; const destination = `{key}-${uuidv4()}`; const nonExistingKey1 = `{key}-${uuidv4()}`; @@ -499,24 +492,24 @@ export function runBaseTests(config: { const value1 = "foobar"; const value2 = "abcdef"; - checkSimple(await client.set(key1, value1)).toEqual("OK"); - checkSimple(await client.set(key2, value2)).toEqual("OK"); + expect(await client.set(key1, value1)).toEqual("OK"); + expect(await client.set(key2, value2)).toEqual("OK"); expect( await client.bitop(BitwiseOperation.AND, destination, keys), ).toEqual(6); - checkSimple(await client.get(destination)).toEqual("`bc`ab"); + expect(await client.get(destination)).toEqual("`bc`ab"); expect( await client.bitop(BitwiseOperation.OR, destination, keys), ).toEqual(6); - checkSimple(await client.get(destination)).toEqual("goofev"); + expect(await client.get(destination)).toEqual("goofev"); // reset values for simplicity of results in XOR - checkSimple(await client.set(key1, "a")).toEqual("OK"); - checkSimple(await client.set(key2, "b")).toEqual("OK"); + expect(await client.set(key1, "a")).toEqual("OK"); + expect(await client.set(key2, "b")).toEqual("OK"); expect( await client.bitop(BitwiseOperation.XOR, destination, keys), ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("\u0003"); + expect(await client.get(destination)).toEqual("\u0003"); // test single source key expect( @@ -524,25 +517,30 @@ export function runBaseTests(config: { key1, ]), ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("a"); + expect(await client.get(destination)).toEqual("a"); expect( await client.bitop(BitwiseOperation.OR, destination, [ key1, ]), ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("a"); + expect(await client.get(destination)).toEqual("a"); expect( await client.bitop(BitwiseOperation.XOR, destination, [ key1, ]), ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("a"); + expect(await client.get(destination)).toEqual("a"); + + // Sets to a string (not a space character) with value 11000010 10011110. + expect(await client.set(key3, "ž")).toEqual("OK"); + expect(await client.getbit(key3, 0)).toEqual(1); expect( await client.bitop(BitwiseOperation.NOT, destination, [ - key1, + key3, ]), - ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("�"); + ).toEqual(2); + // Value becomes 00111101 01100001. + expect(await client.get(destination)).toEqual("=a"); expect(await client.setbit(key1, 0, 1)).toEqual(0); expect( @@ -550,7 +548,7 @@ export function runBaseTests(config: { key1, ]), ).toEqual(1); - checkSimple(await client.get(destination)).toEqual("\u001e"); + expect(await client.get(destination)).toEqual("\u001e"); // stores null when all keys hold empty strings expect( @@ -674,7 +672,7 @@ export function runBaseTests(config: { const setKey = `{key}-${uuidv4()}`; const value = "?f0obar"; // 00111111 01100110 00110000 01101111 01100010 01100001 01110010 - checkSimple(await client.set(key, value)).toEqual("OK"); + expect(await client.set(key, value)).toEqual("OK"); expect(await client.bitpos(key, 0)).toEqual(0); expect(await client.bitpos(key, 1)).toEqual(2); expect(await client.bitpos(key, 1, 1)).toEqual(9); @@ -811,7 +809,7 @@ export function runBaseTests(config: { const overflowGet = new BitFieldGet(u2, offset1); // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 - checkSimple(await client.set(key1, foobar)).toEqual("OK"); + expect(await client.set(key1, foobar)).toEqual("OK"); // SET tests expect( @@ -968,7 +966,7 @@ export function runBaseTests(config: { ); // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 - checkSimple(await client.set(key, foobar)).toEqual("OK"); + expect(await client.set(key, foobar)).toEqual("OK"); expect( await client.bitfieldReadOnly(key, [ // Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 @@ -1075,15 +1073,15 @@ export function runBaseTests(config: { const prevTimeout = (await client.configGet([ "timeout", ])) as Record; - checkSimple( - await client.configSet({ timeout: "1000" }), - ).toEqual("OK"); + expect(await client.configSet({ timeout: "1000" })).toEqual( + "OK", + ); const currTimeout = (await client.configGet([ "timeout", ])) as Record; - checkSimple(currTimeout).toEqual({ timeout: "1000" }); + expect(currTimeout).toEqual({ timeout: "1000" }); /// Revert to the pervious configuration - checkSimple( + expect( await client.configSet({ timeout: prevTimeout["timeout"], }), @@ -1102,7 +1100,7 @@ export function runBaseTests(config: { const key2 = uuidv4(); expect(await client.set(key1, value1)).toEqual("OK"); - checkSimple(await client.getdel(key1)).toEqual(value1); + expect(await client.getdel(key1)).toEqual(value1); expect(await client.getdel(key1)).toEqual(null); // key isn't a string @@ -1126,8 +1124,8 @@ export function runBaseTests(config: { [field2]: value, }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - checkSimple(await client.hget(key, field1)).toEqual(value); - checkSimple(await client.hget(key, field2)).toEqual(value); + expect(await client.hget(key, field1)).toEqual(value); + expect(await client.hget(key, field2)).toEqual(value); expect(await client.hget(key, "nonExistingField")).toEqual( null, ); @@ -1175,7 +1173,7 @@ export function runBaseTests(config: { [field2]: value, }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - checkSimple( + expect( await client.hmget(key, [ field1, "nonExistingField", @@ -1228,12 +1226,10 @@ export function runBaseTests(config: { }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(intoString(await client.hgetall(key))).toEqual( - intoString({ - [field1]: value, - [field2]: value, - }), - ); + expect(await client.hgetall(key)).toEqual({ + [field1]: value, + [field2]: value, + }); expect(await client.hgetall("nonExistingKey")).toEqual({}); }, protocol); @@ -1355,12 +1351,9 @@ export function runBaseTests(config: { }; expect(await client.hset(key1, fieldValueMap)).toEqual(2); - checkSimple(await client.hvals(key1)).toEqual([ - "value1", - "value2", - ]); + expect(await client.hvals(key1)).toEqual(["value1", "value2"]); expect(await client.hdel(key1, [field1])).toEqual(1); - checkSimple(await client.hvals(key1)).toEqual(["value2"]); + expect(await client.hvals(key1)).toEqual(["value2"]); expect(await client.hvals("nonExistingHash")).toEqual([]); }, protocol); }, @@ -1379,9 +1372,9 @@ export function runBaseTests(config: { expect(await client.hsetnx(key1, field, "newValue")).toEqual( false, ); - checkSimple(await client.hget(key1, field)).toEqual("value"); + expect(await client.hget(key1, field)).toEqual("value"); - checkSimple(await client.set(key2, "value")).toEqual("OK"); + expect(await client.set(key2, "value")).toEqual("OK"); await expect( client.hsetnx(key2, field, "value"), ).rejects.toThrow(); @@ -1408,7 +1401,7 @@ export function runBaseTests(config: { expect(await client.hstrlen(key2, "field")).toBe(0); // key exists but holds non hash type value - checkSimple(await client.set(key2, "value")).toEqual("OK"); + expect(await client.set(key2, "value")).toEqual("OK"); await expect(client.hstrlen(key2, field)).rejects.toThrow( RequestError, ); @@ -1424,13 +1417,13 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value4", "value3", "value2", "value1"]; expect(await client.lpush(key, valueList)).toEqual(4); - checkSimple(await client.lpop(key)).toEqual("value1"); - checkSimple(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.lpop(key)).toEqual("value1"); + expect(await client.lrange(key, 0, -1)).toEqual([ "value2", "value3", "value4", ]); - checkSimple(await client.lpopCount(key, 2)).toEqual([ + expect(await client.lpopCount(key, 2)).toEqual([ "value2", "value3", ]); @@ -1448,7 +1441,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.lpush(key, ["bar"])).toThrow(); @@ -1488,7 +1481,7 @@ export function runBaseTests(config: { expect(await client.lpush(key1, ["0"])).toEqual(1); expect(await client.lpushx(key1, ["1", "2", "3"])).toEqual(4); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + expect(await client.lrange(key1, 0, -1)).toEqual([ "3", "2", "1", @@ -1496,10 +1489,10 @@ export function runBaseTests(config: { ]); expect(await client.lpushx(key2, ["1"])).toEqual(0); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); + expect(await client.lrange(key2, 0, -1)).toEqual([]); // Key exists, but is not a list - checkSimple(await client.set(key3, "bar")); + expect(await client.set(key3, "bar")); await expect(client.lpushx(key3, ["_"])).rejects.toThrow( RequestError, ); @@ -1525,7 +1518,7 @@ export function runBaseTests(config: { expect(await client.llen("nonExistingKey")).toEqual(0); - checkSimple(await client.set(key2, "foo")).toEqual("OK"); + expect(await client.set(key2, "foo")).toEqual("OK"); try { expect(await client.llen(key2)).toThrow(); @@ -1557,7 +1550,7 @@ export function runBaseTests(config: { expect(await client.lpush(key2, lpushArgs2)).toEqual(2); // Move from LEFT to LEFT - checkSimple( + expect( await client.lmove( key1, key2, @@ -1567,7 +1560,7 @@ export function runBaseTests(config: { ).toEqual("1"); // Move from LEFT to RIGHT - checkSimple( + expect( await client.lmove( key1, key2, @@ -1576,16 +1569,16 @@ export function runBaseTests(config: { ), ).toEqual("2"); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + expect(await client.lrange(key2, 0, -1)).toEqual([ "1", "3", "4", "2", ]); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([]); + expect(await client.lrange(key1, 0, -1)).toEqual([]); // Move from RIGHT to LEFT - non-existing destination key - checkSimple( + expect( await client.lmove( key2, key1, @@ -1595,7 +1588,7 @@ export function runBaseTests(config: { ).toEqual("2"); // Move from RIGHT to RIGHT - checkSimple( + expect( await client.lmove( key2, key1, @@ -1604,14 +1597,8 @@ export function runBaseTests(config: { ), ).toEqual("4"); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([ - "1", - "3", - ]); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([ - "2", - "4", - ]); + expect(await client.lrange(key2, 0, -1)).toEqual(["1", "3"]); + expect(await client.lrange(key1, 0, -1)).toEqual(["2", "4"]); // Non-existing source key expect( @@ -1625,7 +1612,7 @@ export function runBaseTests(config: { // Non-list source key const key3 = "{key}-3" + uuidv4(); - checkSimple(await client.set(key3, "value")).toEqual("OK"); + expect(await client.set(key3, "value")).toEqual("OK"); await expect( client.lmove( key3, @@ -1803,18 +1790,16 @@ export function runBaseTests(config: { ).rejects.toThrow(RequestError); // assert lset result - checkSimple(await client.lset(key, index, element)).toEqual( - "OK", - ); - checkSimple(await client.lrange(key, 0, negativeIndex)).toEqual( + expect(await client.lset(key, index, element)).toEqual("OK"); + expect(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( + expect(await client.lset(key, negativeIndex, element)).toEqual( + "OK", + ); + expect(await client.lrange(key, 0, negativeIndex)).toEqual( expectedList2, ); @@ -1837,17 +1822,17 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value4", "value3", "value2", "value1"]; expect(await client.lpush(key, valueList)).toEqual(4); - checkSimple(await client.ltrim(key, 0, 1)).toEqual("OK"); - checkSimple(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.ltrim(key, 0, 1)).toEqual("OK"); + expect(await client.lrange(key, 0, -1)).toEqual([ "value1", "value2", ]); /// `start` is greater than `end` so the key will be removed. - checkSimple(await client.ltrim(key, 4, 2)).toEqual("OK"); + expect(await client.ltrim(key, 4, 2)).toEqual("OK"); expect(await client.lrange(key, 0, -1)).toEqual([]); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.ltrim(key, 0, 1)).toThrow(); @@ -1875,20 +1860,18 @@ export function runBaseTests(config: { ]; expect(await client.lpush(key, valueList)).toEqual(5); expect(await client.lrem(key, 2, "value1")).toEqual(2); - checkSimple(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.lrange(key, 0, -1)).toEqual([ "value2", "value2", "value1", ]); expect(await client.lrem(key, -1, "value2")).toEqual(1); - checkSimple(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.lrange(key, 0, -1)).toEqual([ "value2", "value1", ]); expect(await client.lrem(key, 0, "value2")).toEqual(1); - checkSimple(await client.lrange(key, 0, -1)).toEqual([ - "value1", - ]); + expect(await client.lrange(key, 0, -1)).toEqual(["value1"]); expect(await client.lrem("nonExistingKey", 2, "value")).toEqual( 0, ); @@ -1904,8 +1887,8 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value1", "value2", "value3", "value4"]; expect(await client.rpush(key, valueList)).toEqual(4); - checkSimple(await client.rpop(key)).toEqual("value4"); - checkSimple(await client.rpopCount(key, 2)).toEqual([ + expect(await client.rpop(key)).toEqual("value4"); + expect(await client.rpopCount(key, 2)).toEqual([ "value3", "value2", ]); @@ -1920,7 +1903,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.rpush(key, ["bar"])).toThrow(); @@ -1952,7 +1935,7 @@ export function runBaseTests(config: { 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([ + expect(await client.lrange(key1, 0, -1)).toEqual([ "0", "1", "2", @@ -1960,10 +1943,10 @@ export function runBaseTests(config: { ]); expect(await client.rpushx(key2, ["1"])).toEqual(0); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); + expect(await client.lrange(key2, 0, -1)).toEqual([]); // Key exists, but is not a list - checkSimple(await client.set(key3, "bar")); + expect(await client.set(key3, "bar")); await expect(client.rpushx(key3, ["_"])).rejects.toThrow( RequestError, ); @@ -1988,7 +1971,7 @@ export function runBaseTests(config: { await client.srem(key, ["member3", "nonExistingMember"]), ).toEqual(1); /// compare the 2 sets. - checkSimple(await client.smembers(key)).toEqual( + expect(await client.smembers(key)).toEqual( new Set(["member1", "member2", "member4"]), ); expect(await client.srem(key, ["member1"])).toEqual(1); @@ -2013,19 +1996,19 @@ export function runBaseTests(config: { // move an element expect(await client.smove(key1, key2, "1")); - checkSimple(await client.smembers(key1)).toEqual( + expect(await client.smembers(key1)).toEqual( new Set(["2", "3"]), ); - checkSimple(await client.smembers(key2)).toEqual( + expect(await client.smembers(key2)).toEqual( new Set(["1", "2", "3"]), ); // moved element already exists in the destination set expect(await client.smove(key2, key1, "2")); - checkSimple(await client.smembers(key1)).toEqual( + expect(await client.smembers(key1)).toEqual( new Set(["2", "3"]), ); - checkSimple(await client.smembers(key2)).toEqual( + expect(await client.smembers(key2)).toEqual( new Set(["1", "3"]), ); @@ -2033,43 +2016,29 @@ export function runBaseTests(config: { expect(await client.smove(non_existing_key, key1, "4")).toEqual( false, ); - checkSimple(await client.smembers(key1)).toEqual( + expect(await client.smembers(key1)).toEqual( new Set(["2", "3"]), ); // move to a new set expect(await client.smove(key1, key3, "2")); - checkSimple(await client.smembers(key1)).toEqual( - new Set(["3"]), - ); - checkSimple(await client.smembers(key3)).toEqual( - new Set(["2"]), - ); + expect(await client.smembers(key1)).toEqual(new Set(["3"])); + expect(await client.smembers(key3)).toEqual(new Set(["2"])); // attempt to move a missing element expect(await client.smove(key1, key3, "42")).toEqual(false); - checkSimple(await client.smembers(key1)).toEqual( - new Set(["3"]), - ); - checkSimple(await client.smembers(key3)).toEqual( - new Set(["2"]), - ); + expect(await client.smembers(key1)).toEqual(new Set(["3"])); + expect(await client.smembers(key3)).toEqual(new Set(["2"])); // move missing element to missing key expect( await client.smove(key1, non_existing_key, "42"), ).toEqual(false); - checkSimple(await client.smembers(key1)).toEqual( - new Set(["3"]), - ); - checkSimple(await client.type(non_existing_key)).toEqual( - "none", - ); + expect(await client.smembers(key1)).toEqual(new Set(["3"])); + expect(await client.type(non_existing_key)).toEqual("none"); // key exists, but it is not a set - checkSimple(await client.set(string_key, "value")).toEqual( - "OK", - ); + expect(await client.set(string_key, "value")).toEqual("OK"); await expect( client.smove(string_key, key1, "_"), ).rejects.toThrow(); @@ -2099,7 +2068,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.sadd(key, ["bar"])).toThrow(); @@ -2150,7 +2119,7 @@ export function runBaseTests(config: { // positive test case expect(await client.sadd(key1, member1_list)).toEqual(4); expect(await client.sadd(key2, member2_list)).toEqual(3); - checkSimple(await client.sinter([key1, key2])).toEqual( + expect(await client.sinter([key1, key2])).toEqual( new Set(["c", "d"]), ); @@ -2169,7 +2138,7 @@ export function runBaseTests(config: { ); // non-set key - checkSimple(await client.set(key2, "value")).toEqual("OK"); + expect(await client.set(key2, "value")).toEqual("OK"); try { expect(await client.sinter([key2])).toThrow(); @@ -2233,7 +2202,7 @@ export function runBaseTests(config: { ); // source key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.sintercard([key1, stringKey]), ).rejects.toThrow(RequestError); @@ -2259,42 +2228,34 @@ export function runBaseTests(config: { // store in a new key expect(await client.sinterstore(key3, [key1, key2])).toEqual(1); - checkSimple(await client.smembers(key3)).toEqual( - new Set(["c"]), - ); + expect(await client.smembers(key3)).toEqual(new Set(["c"])); // overwrite existing set, which is also a source set expect(await client.sinterstore(key2, [key2, key3])).toEqual(1); - checkSimple(await client.smembers(key2)).toEqual( - new Set(["c"]), - ); + expect(await client.smembers(key2)).toEqual(new Set(["c"])); // source set is the same as the existing set expect(await client.sinterstore(key2, [key2])).toEqual(1); - checkSimple(await client.smembers(key2)).toEqual( - new Set(["c"]), - ); + expect(await client.smembers(key2)).toEqual(new Set(["c"])); // intersection with non-existing key expect( await client.sinterstore(key1, [key2, nonExistingKey]), ).toEqual(0); - checkSimple(await client.smembers(key1)).toEqual(new Set()); + expect(await client.smembers(key1)).toEqual(new Set()); // invalid argument - key list must not be empty await expect(client.sinterstore(key3, [])).rejects.toThrow(); // non-set key - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.sinterstore(key3, [stringKey]), ).rejects.toThrow(); // overwrite non-set key expect(await client.sinterstore(stringKey, [key2])).toEqual(1); - checkSimple(await client.smembers(stringKey)).toEqual( - new Set("c"), - ); + expect(await client.smembers(stringKey)).toEqual(new Set("c")); }, protocol); }, config.timeout, @@ -2314,17 +2275,17 @@ export function runBaseTests(config: { expect(await client.sadd(key1, member1_list)).toEqual(3); expect(await client.sadd(key2, member2_list)).toEqual(3); - checkSimple(await client.sdiff([key1, key2])).toEqual( + expect(await client.sdiff([key1, key2])).toEqual( new Set(["a", "b"]), ); - checkSimple(await client.sdiff([key2, key1])).toEqual( + expect(await client.sdiff([key2, key1])).toEqual( new Set(["d", "e"]), ); - checkSimple(await client.sdiff([key1, nonExistingKey])).toEqual( + expect(await client.sdiff([key1, nonExistingKey])).toEqual( new Set(["a", "b", "c"]), ); - checkSimple(await client.sdiff([nonExistingKey, key1])).toEqual( + expect(await client.sdiff([nonExistingKey, key1])).toEqual( new Set(), ); @@ -2332,7 +2293,7 @@ export function runBaseTests(config: { await expect(client.sdiff([])).rejects.toThrow(); // key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect(client.sdiff([stringKey])).rejects.toThrow(); }, protocol); }, @@ -2356,27 +2317,25 @@ export function runBaseTests(config: { // store diff in new key expect(await client.sdiffstore(key3, [key1, key2])).toEqual(2); - checkSimple(await client.smembers(key3)).toEqual( + expect(await client.smembers(key3)).toEqual( new Set(["a", "b"]), ); // overwrite existing set expect(await client.sdiffstore(key3, [key2, key1])).toEqual(2); - checkSimple(await client.smembers(key3)).toEqual( + expect(await client.smembers(key3)).toEqual( new Set(["d", "e"]), ); // overwrite one of the source sets expect(await client.sdiffstore(key3, [key2, key3])).toEqual(1); - checkSimple(await client.smembers(key3)).toEqual( - new Set(["c"]), - ); + expect(await client.smembers(key3)).toEqual(new Set(["c"])); // diff between non-empty set and empty set expect( await client.sdiffstore(key3, [key1, nonExistingKey]), ).toEqual(3); - checkSimple(await client.smembers(key3)).toEqual( + expect(await client.smembers(key3)).toEqual( new Set(["a", "b", "c"]), ); @@ -2384,13 +2343,13 @@ export function runBaseTests(config: { expect( await client.sdiffstore(key3, [nonExistingKey, key1]), ).toEqual(0); - checkSimple(await client.smembers(key3)).toEqual(new Set()); + expect(await client.smembers(key3)).toEqual(new Set()); // invalid argument - key list must not be empty await expect(client.sdiffstore(key3, [])).rejects.toThrow(); // source key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.sdiffstore(key3, [stringKey]), ).rejects.toThrow(); @@ -2399,7 +2358,7 @@ export function runBaseTests(config: { expect( await client.sdiffstore(stringKey, [key1, key2]), ).toEqual(2); - checkSimple(await client.smembers(stringKey)).toEqual( + expect(await client.smembers(stringKey)).toEqual( new Set(["a", "b"]), ); }, protocol); @@ -2420,7 +2379,7 @@ export function runBaseTests(config: { expect(await client.sadd(key1, memberList1)).toEqual(3); expect(await client.sadd(key2, memberList2)).toEqual(4); - checkSimple(await client.sunion([key1, key2])).toEqual( + expect(await client.sunion([key1, key2])).toEqual( new Set(["a", "b", "c", "d", "e"]), ); @@ -2428,12 +2387,12 @@ export function runBaseTests(config: { await expect(client.sunion([])).rejects.toThrow(); // non-existing key returns the set of existing keys - checkSimple( - await client.sunion([key1, nonExistingKey]), - ).toEqual(new Set(memberList1)); + expect(await client.sunion([key1, nonExistingKey])).toEqual( + new Set(memberList1), + ); // key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect(client.sunion([stringKey])).rejects.toThrow(); }, protocol); }, @@ -2457,19 +2416,19 @@ export function runBaseTests(config: { // store union in new key expect(await client.sunionstore(key4, [key1, key2])).toEqual(5); - checkSimple(await client.smembers(key4)).toEqual( + expect(await client.smembers(key4)).toEqual( new Set(["a", "b", "c", "d", "e"]), ); // overwrite existing set expect(await client.sunionstore(key1, [key4, key2])).toEqual(5); - checkSimple(await client.smembers(key1)).toEqual( + expect(await client.smembers(key1)).toEqual( new Set(["a", "b", "c", "d", "e"]), ); // overwrite one of the source keys expect(await client.sunionstore(key2, [key4, key2])).toEqual(5); - checkSimple(await client.smembers(key2)).toEqual( + expect(await client.smembers(key2)).toEqual( new Set(["a", "b", "c", "d", "e"]), ); @@ -2483,7 +2442,7 @@ export function runBaseTests(config: { await expect(client.sunionstore(key4, [])).rejects.toThrow(); // key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.sunionstore(key4, [stringKey, key1]), ).rejects.toThrow(); @@ -2492,7 +2451,7 @@ export function runBaseTests(config: { expect( await client.sunionstore(stringKey, [key1, key3]), ).toEqual(7); - checkSimple(await client.smembers(stringKey)).toEqual( + expect(await client.smembers(stringKey)).toEqual( new Set(["a", "b", "c", "d", "e", "f", "g"]), ); }, protocol); @@ -2515,7 +2474,7 @@ export function runBaseTests(config: { await client.sismember("nonExistingKey", "member1"), ).toEqual(false); - checkSimple(await client.set(key2, "foo")).toEqual("OK"); + expect(await client.set(key2, "foo")).toEqual("OK"); await expect( client.sismember(key2, "member1"), ).rejects.toThrow(); @@ -2552,7 +2511,7 @@ export function runBaseTests(config: { ); // key exists, but it is not a set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.smismember(stringKey, ["a"]), ).rejects.toThrow(RequestError); @@ -2570,11 +2529,11 @@ export function runBaseTests(config: { expect(await client.sadd(key, members)).toEqual(3); const result1 = await client.spop(key); - expect(members).toContain(intoString(result1)); + expect(members).toContain(result1); members = members.filter((item) => item != result1); const result2 = await client.spopCount(key, 2); - expect(intoString(result2)).toEqual(intoString(members)); + expect(result2).toEqual(new Set(members)); expect(await client.spop("nonExistingKey")).toEqual(null); expect(await client.spopCount("nonExistingKey", 1)).toEqual( new Set(), @@ -2591,9 +2550,9 @@ export function runBaseTests(config: { const key1 = uuidv4(); const key2 = uuidv4(); const value = uuidv4(); - checkSimple(await client.set(key1, value)).toEqual("OK"); + expect(await client.set(key1, value)).toEqual("OK"); expect(await client.exists([key1])).toEqual(1); - checkSimple(await client.set(key2, value)).toEqual("OK"); + expect(await client.set(key2, value)).toEqual("OK"); expect( await client.exists([key1, "nonExistingKey", key2]), ).toEqual(2); @@ -2611,9 +2570,9 @@ export function runBaseTests(config: { const key2 = "{key}" + uuidv4(); const key3 = "{key}" + uuidv4(); const value = uuidv4(); - checkSimple(await client.set(key1, value)).toEqual("OK"); - checkSimple(await client.set(key2, value)).toEqual("OK"); - checkSimple(await client.set(key3, value)).toEqual("OK"); + expect(await client.set(key1, value)).toEqual("OK"); + expect(await client.set(key2, value)).toEqual("OK"); + expect(await client.set(key3, value)).toEqual("OK"); expect( await client.unlink([key1, key2, "nonExistingKey", key3]), ).toEqual(3); @@ -2627,11 +2586,11 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.expire(key, 10)).toEqual(true); expect(await client.ttl(key)).toBeLessThanOrEqual(10); /// set command clears the timeout. - checkSimple(await client.set(key, "bar")).toEqual("OK"); + expect(await client.set(key, "bar")).toEqual("OK"); const versionLessThan = cluster.checkIfServerVersionLessThan("7.0.0"); @@ -2673,7 +2632,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect( await client.expireAt( key, @@ -2704,7 +2663,7 @@ export function runBaseTests(config: { expect(await client.ttl(key)).toBeLessThanOrEqual(50); /// set command clears the timeout. - checkSimple(await client.set(key, "bar")).toEqual("OK"); + expect(await client.set(key, "bar")).toEqual("OK"); if (!versionLessThan) { expect( @@ -2725,14 +2684,14 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.ttl(key)).toEqual(-1); expect(await client.expire(key, -10)).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.pexpire(key, -10000)).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect( await client.expireAt( key, @@ -2740,7 +2699,7 @@ export function runBaseTests(config: { ), ).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect( await client.pexpireAt( key, @@ -2786,12 +2745,12 @@ export function runBaseTests(config: { const key2 = Buffer.from(uuidv4()); let script = new Script(Buffer.from("return 'Hello'")); - checkSimple(await client.invokeScript(script)).toEqual("Hello"); + expect(await client.invokeScript(script)).toEqual("Hello"); script = new Script( Buffer.from("return redis.call('SET', KEYS[1], ARGV[1])"), ); - checkSimple( + expect( await client.invokeScript(script, { keys: [key1], args: [Buffer.from("value1")], @@ -2799,7 +2758,7 @@ export function runBaseTests(config: { ).toEqual("OK"); /// Reuse the same script with different parameters. - checkSimple( + expect( await client.invokeScript(script, { keys: [key2], args: [Buffer.from("value2")], @@ -2809,11 +2768,11 @@ export function runBaseTests(config: { script = new Script( Buffer.from("return redis.call('GET', KEYS[1])"), ); - checkSimple( + expect( await client.invokeScript(script, { keys: [key1] }), ).toEqual("value1"); - checkSimple( + expect( await client.invokeScript(script, { keys: [key2] }), ).toEqual("value2"); }, protocol); @@ -2829,12 +2788,12 @@ export function runBaseTests(config: { const key2 = uuidv4(); let script = new Script("return 'Hello'"); - checkSimple(await client.invokeScript(script)).toEqual("Hello"); + expect(await client.invokeScript(script)).toEqual("Hello"); script = new Script( "return redis.call('SET', KEYS[1], ARGV[1])", ); - checkSimple( + expect( await client.invokeScript(script, { keys: [key1], args: ["value1"], @@ -2842,7 +2801,7 @@ export function runBaseTests(config: { ).toEqual("OK"); /// Reuse the same script with different parameters. - checkSimple( + expect( await client.invokeScript(script, { keys: [key2], args: ["value2"], @@ -2850,11 +2809,11 @@ export function runBaseTests(config: { ).toEqual("OK"); script = new Script("return redis.call('GET', KEYS[1])"); - checkSimple( + expect( await client.invokeScript(script, { keys: [key1] }), ).toEqual("value1"); - checkSimple( + expect( await client.invokeScript(script, { keys: [key2] }), ).toEqual("value2"); }, protocol); @@ -3064,14 +3023,12 @@ export function runBaseTests(config: { expect(await client.zadd(key2, entries2)).toEqual(1); expect(await client.zadd(key3, entries3)).toEqual(4); - checkSimple(await client.zdiff([key1, key2])).toEqual([ + expect(await client.zdiff([key1, key2])).toEqual([ "one", "three", ]); - checkSimple(await client.zdiff([key1, key3])).toEqual([]); - checkSimple(await client.zdiff([nonExistingKey, key3])).toEqual( - [], - ); + expect(await client.zdiff([key1, key3])).toEqual([]); + expect(await client.zdiff([nonExistingKey, key3])).toEqual([]); let result = await client.zdiffWithScores([key1, key2]); const expected = { @@ -3093,7 +3050,7 @@ export function runBaseTests(config: { ); // key exists, but it is not a sorted set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect(client.zdiff([stringKey, key1])).rejects.toThrow(); await expect( client.zdiffWithScores([stringKey, key1]), @@ -3174,7 +3131,7 @@ export function runBaseTests(config: { ); // key exists, but it is not a sorted set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.zdiffstore(key4, [stringKey, key1]), ).rejects.toThrow(RequestError); @@ -3199,7 +3156,7 @@ export function runBaseTests(config: { await client.zscore("nonExistingKey", "nonExistingMember"), ).toEqual(null); - checkSimple(await client.set(key2, "foo")).toEqual("OK"); + expect(await client.set(key2, "foo")).toEqual("OK"); await expect(client.zscore(key2, "foo")).rejects.toThrow(); }, protocol); }, @@ -3246,7 +3203,7 @@ export function runBaseTests(config: { ); // key exists, but it is not a sorted set - checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + expect(await client.set(stringKey, "foo")).toEqual("OK"); await expect( client.zmscore(stringKey, ["one"]), ).rejects.toThrow(RequestError); @@ -3302,7 +3259,7 @@ export function runBaseTests(config: { ), ).toEqual(0); - checkSimple(await client.set(key2, "foo")).toEqual("OK"); + expect(await client.set(key2, "foo")).toEqual("OK"); await expect( client.zcount(key2, "negativeInfinity", "positiveInfinity"), ).rejects.toThrow(); @@ -3319,9 +3276,9 @@ export function runBaseTests(config: { const membersScores = { one: 1, two: 2, three: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - checkSimple( - await client.zrange(key, { start: 0, stop: 1 }), - ).toEqual(["one", "two"]); + expect(await client.zrange(key, { start: 0, stop: 1 })).toEqual( + ["one", "two"], + ); const result = await client.zrangeWithScores(key, { start: 0, stop: -1, @@ -3334,7 +3291,7 @@ export function runBaseTests(config: { three: 3.0, }), ).toBe(true); - checkSimple( + expect( await client.zrange(key, { start: 0, stop: 1 }, true), ).toEqual(["three", "two"]); expect(await client.zrange(key, { start: 3, stop: 1 })).toEqual( @@ -3356,7 +3313,7 @@ export function runBaseTests(config: { const membersScores = { one: 1, two: 2, three: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - checkSimple( + expect( await client.zrange(key, { start: "negativeInfinity", stop: { value: 3, isInclusive: false }, @@ -3376,7 +3333,7 @@ export function runBaseTests(config: { three: 3.0, }), ).toBe(true); - checkSimple( + expect( await client.zrange( key, { @@ -3388,7 +3345,7 @@ export function runBaseTests(config: { ), ).toEqual(["two", "one"]); - checkSimple( + expect( await client.zrange(key, { start: "negativeInfinity", stop: "positiveInfinity", @@ -3449,7 +3406,7 @@ export function runBaseTests(config: { const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - checkSimple( + expect( await client.zrange(key, { start: "negativeInfinity", stop: { value: "c", isInclusive: false }, @@ -3457,7 +3414,7 @@ export function runBaseTests(config: { }), ).toEqual(["a", "b"]); - checkSimple( + expect( await client.zrange(key, { start: "negativeInfinity", stop: "positiveInfinity", @@ -3466,7 +3423,7 @@ export function runBaseTests(config: { }), ).toEqual(["b", "c"]); - checkSimple( + expect( await client.zrange( key, { @@ -3675,27 +3632,25 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "value")).toEqual("OK"); - checkSimple(await client.type(key)).toEqual("string"); - checkSimple(await client.del([key])).toEqual(1); + expect(await client.set(key, "value")).toEqual("OK"); + expect(await client.type(key)).toEqual("string"); + expect(await client.del([key])).toEqual(1); - checkSimple(await client.lpush(key, ["value"])).toEqual(1); - checkSimple(await client.type(key)).toEqual("list"); - checkSimple(await client.del([key])).toEqual(1); + expect(await client.lpush(key, ["value"])).toEqual(1); + expect(await client.type(key)).toEqual("list"); + expect(await client.del([key])).toEqual(1); - checkSimple(await client.sadd(key, ["value"])).toEqual(1); - checkSimple(await client.type(key)).toEqual("set"); - checkSimple(await client.del([key])).toEqual(1); + expect(await client.sadd(key, ["value"])).toEqual(1); + expect(await client.type(key)).toEqual("set"); + expect(await client.del([key])).toEqual(1); - checkSimple(await client.zadd(key, { member: 1.0 })).toEqual(1); - checkSimple(await client.type(key)).toEqual("zset"); - checkSimple(await client.del([key])).toEqual(1); + expect(await client.zadd(key, { member: 1.0 })).toEqual(1); + expect(await client.type(key)).toEqual("zset"); + expect(await client.del([key])).toEqual(1); - checkSimple(await client.hset(key, { field: "value" })).toEqual( - 1, - ); - checkSimple(await client.type(key)).toEqual("hash"); - checkSimple(await client.del([key])).toEqual(1); + expect(await client.hset(key, { field: "value" })).toEqual(1); + expect(await client.type(key)).toEqual("hash"); + expect(await client.del([key])).toEqual(1); await client.customCommand([ "XADD", @@ -3704,9 +3659,9 @@ export function runBaseTests(config: { "field", "value", ]); - checkSimple(await client.type(key)).toEqual("stream"); - checkSimple(await client.del([key])).toEqual(1); - checkSimple(await client.type(key)).toEqual("none"); + expect(await client.type(key)).toEqual("stream"); + expect(await client.del([key])).toEqual(1); + expect(await client.type(key)).toEqual("none"); }, protocol); }, config.timeout, @@ -3717,7 +3672,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const message = uuidv4(); - checkSimple(await client.echo(message)).toEqual(message); + expect(await client.echo(message)).toEqual(message); }, protocol); }, config.timeout, @@ -3730,8 +3685,8 @@ export function runBaseTests(config: { const key1 = uuidv4(); const key1Value = uuidv4(); const key1ValueLength = key1Value.length; - checkSimple(await client.set(key1, key1Value)).toEqual("OK"); - checkSimple(await client.strlen(key1)).toEqual(key1ValueLength); + expect(await client.set(key1, key1Value)).toEqual("OK"); + expect(await client.strlen(key1)).toEqual(key1ValueLength); expect(await client.strlen("nonExistKey")).toEqual(0); @@ -3765,12 +3720,8 @@ export function runBaseTests(config: { listKey2Value, ]), ).toEqual(2); - checkSimple(await client.lindex(listName, 0)).toEqual( - listKey2Value, - ); - checkSimple(await client.lindex(listName, 1)).toEqual( - listKey1Value, - ); + expect(await client.lindex(listName, 0)).toEqual(listKey2Value); + expect(await client.lindex(listName, 1)).toEqual(listKey1Value); expect(await client.lindex("notExsitingList", 1)).toEqual(null); expect(await client.lindex(listName, 3)).toEqual(null); }, protocol); @@ -3805,7 +3756,7 @@ export function runBaseTests(config: { "3.5", ), ).toEqual(6); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + expect(await client.lrange(key1, 0, -1)).toEqual([ "1", "1.5", "2", @@ -3857,7 +3808,7 @@ export function runBaseTests(config: { }), ).toBe(true); expect(await client.zpopmin(key)).toEqual({}); - checkSimple(await client.set(key, "value")).toEqual("OK"); + expect(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmin(key)).rejects.toThrow(); expect(await client.zpopmin("notExsitingKey")).toEqual({}); }, protocol); @@ -3881,7 +3832,7 @@ export function runBaseTests(config: { }), ).toBe(true); expect(await client.zpopmax(key)).toEqual({}); - checkSimple(await client.set(key, "value")).toEqual("OK"); + expect(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmax(key)).rejects.toThrow(); expect(await client.zpopmax("notExsitingKey")).toEqual({}); }, protocol); @@ -3896,7 +3847,7 @@ export function runBaseTests(config: { const key = uuidv4(); expect(await client.pttl(key)).toEqual(-2); - checkSimple(await client.set(key, "value")).toEqual("OK"); + expect(await client.set(key, "value")).toEqual("OK"); expect(await client.pttl(key)).toEqual(-1); expect(await client.expire(key, 10)).toEqual(true); @@ -3972,7 +3923,7 @@ export function runBaseTests(config: { null, ); - checkSimple(await client.set(key2, "value")).toEqual("OK"); + expect(await client.set(key2, "value")).toEqual("OK"); await expect(client.zrank(key2, "member")).rejects.toThrow(); }, protocol); }, @@ -4014,7 +3965,7 @@ export function runBaseTests(config: { ).toBeNull(); // Key exists, but is not a sorted set - checkSimple(await client.set(nonSetKey, "value")).toEqual("OK"); + expect(await client.set(nonSetKey, "value")).toEqual("OK"); await expect( client.zrevrank(nonSetKey, "member"), ).rejects.toThrow(); @@ -4030,7 +3981,7 @@ export function runBaseTests(config: { await client.rpush("brpop-test", ["foo", "bar", "baz"]), ).toEqual(3); // Test basic usage - checkSimple(await client.brpop(["brpop-test"], 0.1)).toEqual([ + expect(await client.brpop(["brpop-test"], 0.1)).toEqual([ "brpop-test", "baz", ]); @@ -4067,7 +4018,7 @@ export function runBaseTests(config: { await client.rpush("blpop-test", ["foo", "bar", "baz"]), ).toEqual(3); // Test basic usage - checkSimple(await client.blpop(["blpop-test"], 0.1)).toEqual([ + expect(await client.blpop(["blpop-test"], 0.1)).toEqual([ "blpop-test", "foo", ]); @@ -4101,7 +4052,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.set(key, "foo")).toEqual("OK"); + expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.persist(key)).toEqual(false); expect(await client.expire(key, 10)).toEqual(true); @@ -4141,7 +4092,7 @@ export function runBaseTests(config: { ], { id: "0-1" }, ); - checkSimple(timestamp1).toEqual("0-1"); + expect(timestamp1).toEqual("0-1"); expect( await client.xadd(key, [ [field1, "foo2"], @@ -4338,7 +4289,7 @@ export function runBaseTests(config: { [timestamp_2_3 as string]: [["bar", "bar3"]], }, }; - checkSimple(result).toEqual(expected); + expect(result).toEqual(expected); }, ProtocolVersion.RESP2); }, config.timeout, @@ -4354,7 +4305,7 @@ export function runBaseTests(config: { await client.set(key, "value"); await client.rename(key, newKey); const result = await client.get(newKey); - checkSimple(result).toEqual("value"); + expect(result).toEqual("value"); // If key doesn't exist it should throw, it also test that key has successfully been renamed await expect(client.rename(key, newKey)).rejects.toThrow(); }, protocol); @@ -4381,13 +4332,13 @@ export function runBaseTests(config: { await client.set(key1, "key1"); await client.set(key3, "key3"); // Test that renamenx can rename key1 to key2 (non-existing value) - checkSimple(await client.renamenx(key1, key2)).toEqual(true); + expect(await client.renamenx(key1, key2)).toEqual(true); // sanity check - checkSimple(await client.get(key2)).toEqual("key1"); + expect(await client.get(key2)).toEqual("key1"); // Test that renamenx doesn't rename key2 to key3 (with an existing value) - checkSimple(await client.renamenx(key2, key3)).toEqual(false); + expect(await client.renamenx(key2, key3)).toEqual(false); // sanity check - checkSimple(await client.get(key3)).toEqual("key3"); + expect(await client.get(key3)).toEqual("key3"); }, protocol); }, config.timeout, @@ -4398,13 +4349,13 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - checkSimple(await client.pfadd(key, [])).toEqual(1); - checkSimple(await client.pfadd(key, ["one", "two"])).toEqual(1); - checkSimple(await client.pfadd(key, ["two"])).toEqual(0); - checkSimple(await client.pfadd(key, [])).toEqual(0); + expect(await client.pfadd(key, [])).toEqual(1); + expect(await client.pfadd(key, ["one", "two"])).toEqual(1); + expect(await client.pfadd(key, ["two"])).toEqual(0); + expect(await client.pfadd(key, [])).toEqual(0); // key exists, but it is not a HyperLogLog - checkSimple(await client.set("foo", "value")).toEqual("OK"); + expect(await client.set("foo", "value")).toEqual("OK"); await expect(client.pfadd("foo", [])).rejects.toThrow(); }, protocol); }, @@ -4462,9 +4413,9 @@ export function runBaseTests(config: { count: 500, }, }); - checkSimple(setResWithExpirySetMilli).toEqual("OK"); + expect(setResWithExpirySetMilli).toEqual("OK"); const getWithExpirySetMilli = await client.get(key); - checkSimple(getWithExpirySetMilli).toEqual(value); + expect(getWithExpirySetMilli).toEqual(value); const setResWithExpirySec = await client.set(key, value, { expiry: { @@ -4472,9 +4423,9 @@ export function runBaseTests(config: { count: 1, }, }); - checkSimple(setResWithExpirySec).toEqual("OK"); + expect(setResWithExpirySec).toEqual("OK"); const getResWithExpirySec = await client.get(key); - checkSimple(getResWithExpirySec).toEqual(value); + expect(getResWithExpirySec).toEqual(value); const setWithUnixSec = await client.set(key, value, { expiry: { @@ -4482,59 +4433,59 @@ export function runBaseTests(config: { count: Math.floor(Date.now() / 1000) + 1, }, }); - checkSimple(setWithUnixSec).toEqual("OK"); + expect(setWithUnixSec).toEqual("OK"); const getWithUnixSec = await client.get(key); - checkSimple(getWithUnixSec).toEqual(value); + expect(getWithUnixSec).toEqual(value); const setResWithExpiryKeep = await client.set(key, value, { expiry: "keepExisting", }); - checkSimple(setResWithExpiryKeep).toEqual("OK"); + expect(setResWithExpiryKeep).toEqual("OK"); const getResWithExpiryKeep = await client.get(key); - checkSimple(getResWithExpiryKeep).toEqual(value); + expect(getResWithExpiryKeep).toEqual(value); // wait for the key to expire base on the previous set let sleep = new Promise((resolve) => setTimeout(resolve, 1000)); await sleep; const getResExpire = await client.get(key); // key should have expired - checkSimple(getResExpire).toEqual(null); + expect(getResExpire).toEqual(null); const setResWithExpiryWithUmilli = await client.set(key, value, { expiry: { type: "unixMilliseconds", count: Date.now() + 1000, }, }); - checkSimple(setResWithExpiryWithUmilli).toEqual("OK"); + expect(setResWithExpiryWithUmilli).toEqual("OK"); // wait for the key to expire sleep = new Promise((resolve) => setTimeout(resolve, 1001)); await sleep; const getResWithExpiryWithUmilli = await client.get(key); // key should have expired - checkSimple(getResWithExpiryWithUmilli).toEqual(null); + expect(getResWithExpiryWithUmilli).toEqual(null); } async function setWithOnlyIfExistOptions(client: BaseClient) { const key = uuidv4(); const value = uuidv4(); const setKey = await client.set(key, value); - checkSimple(setKey).toEqual("OK"); + expect(setKey).toEqual("OK"); const getRes = await client.get(key); - checkSimple(getRes).toEqual(value); + expect(getRes).toEqual(value); const setExistingKeyRes = await client.set(key, value, { conditionalSet: "onlyIfExists", }); - checkSimple(setExistingKeyRes).toEqual("OK"); + expect(setExistingKeyRes).toEqual("OK"); const getExistingKeyRes = await client.get(key); - checkSimple(getExistingKeyRes).toEqual(value); + expect(getExistingKeyRes).toEqual(value); const notExistingKeyRes = await client.set(key + 1, value, { conditionalSet: "onlyIfExists", }); // key does not exist, so it should not be set - checkSimple(notExistingKeyRes).toEqual(null); + expect(notExistingKeyRes).toEqual(null); const getNotExistingKey = await client.get(key + 1); // key should not have been set - checkSimple(getNotExistingKey).toEqual(null); + expect(getNotExistingKey).toEqual(null); } async function setWithOnlyIfNotExistOptions(client: BaseClient) { @@ -4544,19 +4495,19 @@ export function runBaseTests(config: { conditionalSet: "onlyIfDoesNotExist", }); // key does not exist, so it should be set - checkSimple(notExistingKeyRes).toEqual("OK"); + expect(notExistingKeyRes).toEqual("OK"); const getNotExistingKey = await client.get(key); // key should have been set - checkSimple(getNotExistingKey).toEqual(value); + expect(getNotExistingKey).toEqual(value); const existingKeyRes = await client.set(key, value, { conditionalSet: "onlyIfDoesNotExist", }); // key exists, so it should not be set - checkSimple(existingKeyRes).toEqual(null); + expect(existingKeyRes).toEqual(null); const getExistingKey = await client.get(key); // key should not have been set - checkSimple(getExistingKey).toEqual(value); + expect(getExistingKey).toEqual(value); } async function setWithGetOldOptions(client: BaseClient) { @@ -4567,19 +4518,19 @@ export function runBaseTests(config: { returnOldValue: true, }); // key does not exist, so old value should be null - checkSimple(setResGetNotExistOld).toEqual(null); + expect(setResGetNotExistOld).toEqual(null); // key should have been set const getResGetNotExistOld = await client.get(key); - checkSimple(getResGetNotExistOld).toEqual(value); + expect(getResGetNotExistOld).toEqual(value); const setResGetExistOld = await client.set(key, value, { returnOldValue: true, }); // key exists, so old value should be returned - checkSimple(setResGetExistOld).toEqual(value); + expect(setResGetExistOld).toEqual(value); // key should have been set const getResGetExistOld = await client.get(key); - checkSimple(getResGetExistOld).toEqual(value); + expect(getResGetExistOld).toEqual(value); } async function setWithAllOptions(client: BaseClient) { @@ -4633,14 +4584,14 @@ export function runBaseTests(config: { }); if (exist == false) { - checkSimple(setRes).toEqual("OK"); + expect(setRes).toEqual("OK"); exist = true; } else { - checkSimple(setRes).toEqual(null); + expect(setRes).toEqual(null); } const getRes = await client.get(key); - checkSimple(getRes).toEqual(value); + expect(getRes).toEqual(value); } for (const expiryVal of expiryCombination) { @@ -4703,37 +4654,31 @@ export function runBaseTests(config: { null, ); - checkSimple( + expect( await client.set( string_key, "a really loooooooooooooooooooooooooooooooooooooooong value", ), ).toEqual("OK"); - checkSimple(await client.objectEncoding(string_key)).toEqual( - "raw", - ); + expect(await client.objectEncoding(string_key)).toEqual("raw"); - checkSimple(await client.set(string_key, "2")).toEqual("OK"); - checkSimple(await client.objectEncoding(string_key)).toEqual( - "int", - ); + expect(await client.set(string_key, "2")).toEqual("OK"); + expect(await client.objectEncoding(string_key)).toEqual("int"); - checkSimple(await client.set(string_key, "value")).toEqual( - "OK", - ); - checkSimple(await client.objectEncoding(string_key)).toEqual( + expect(await client.set(string_key, "value")).toEqual("OK"); + expect(await client.objectEncoding(string_key)).toEqual( "embstr", ); expect(await client.lpush(list_key, ["1"])).toEqual(1); if (versionLessThan72) { - checkSimple(await client.objectEncoding(list_key)).toEqual( + expect(await client.objectEncoding(list_key)).toEqual( "quicklist", ); } else { - checkSimple(await client.objectEncoding(list_key)).toEqual( + expect(await client.objectEncoding(list_key)).toEqual( "listpack", ); } @@ -4745,23 +4690,23 @@ export function runBaseTests(config: { ).toEqual(1); } - checkSimple(await client.objectEncoding(hashtable_key)).toEqual( + expect(await client.objectEncoding(hashtable_key)).toEqual( "hashtable", ); expect(await client.sadd(intset_key, ["1"])).toEqual(1); - checkSimple(await client.objectEncoding(intset_key)).toEqual( + expect(await client.objectEncoding(intset_key)).toEqual( "intset", ); expect(await client.sadd(set_listpack_key, ["foo"])).toEqual(1); if (versionLessThan72) { - checkSimple( + expect( await client.objectEncoding(set_listpack_key), ).toEqual("hashtable"); } else { - checkSimple( + expect( await client.objectEncoding(set_listpack_key), ).toEqual("listpack"); } @@ -4775,20 +4720,20 @@ export function runBaseTests(config: { ).toEqual(1); } - checkSimple( - await client.objectEncoding(hash_hashtable_key), - ).toEqual("hashtable"); + expect(await client.objectEncoding(hash_hashtable_key)).toEqual( + "hashtable", + ); expect( await client.hset(hash_listpack_key, { "1": "2" }), ).toEqual(1); if (versionLessThan7) { - checkSimple( + expect( await client.objectEncoding(hash_listpack_key), ).toEqual("ziplist"); } else { - checkSimple( + expect( await client.objectEncoding(hash_listpack_key), ).toEqual("listpack"); } @@ -4800,7 +4745,7 @@ export function runBaseTests(config: { ).toEqual(1); } - checkSimple(await client.objectEncoding(skiplist_key)).toEqual( + expect(await client.objectEncoding(skiplist_key)).toEqual( "skiplist", ); @@ -4809,11 +4754,11 @@ export function runBaseTests(config: { ).toEqual(1); if (versionLessThan7) { - checkSimple( + expect( await client.objectEncoding(zset_listpack_key), ).toEqual("ziplist"); } else { - checkSimple( + expect( await client.objectEncoding(zset_listpack_key), ).toEqual("listpack"); } @@ -4821,7 +4766,7 @@ export function runBaseTests(config: { expect( await client.xadd(stream_key, [["field", "value"]]), ).not.toBeNull(); - checkSimple(await client.objectEncoding(stream_key)).toEqual( + expect(await client.objectEncoding(stream_key)).toEqual( "stream", ); }, protocol); @@ -5106,7 +5051,7 @@ export function runBaseTests(config: { const key2 = uuidv4(); const value = "foobar"; - checkSimple(await client.set(key1, value)).toEqual("OK"); + expect(await client.set(key1, value)).toEqual("OK"); expect(await client.bitcount(key1)).toEqual(26); expect( await client.bitcount(key1, { start: 1, end: 1 }), @@ -5345,19 +5290,35 @@ export function runBaseTests(config: { const expectedResult = [ [ members[0], - [56.4413, 3479447370796909, membersCoordinates[0]], + [ + 56.4413, + 3479447370796909, + [15.087267458438873, 37.50266842333162], + ], ], [ members[1], - [190.4424, 3479099956230698, membersCoordinates[1]], + [ + 190.4424, + 3479099956230698, + [13.361389338970184, 38.1155563954963], + ], ], [ members[2], - [279.7403, 3481342659049484, membersCoordinates[2]], + [ + 279.7403, + 3481342659049484, + [17.241510450839996, 38.78813451624225], + ], ], [ members[3], - [279.7405, 3479273021651468, membersCoordinates[3]], + [ + 279.7405, + 3479273021651468, + [12.75848776102066, 38.78813451624225], + ], ], ]; @@ -5372,7 +5333,7 @@ export function runBaseTests(config: { { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, ); // using set to compare, because results are reordrered - checkSimple(new Set(searchResult)).toEqual(membersSet); + expect(new Set(searchResult)).toEqual(membersSet); // order search result searchResult = await client.geosearch( @@ -5381,7 +5342,7 @@ export function runBaseTests(config: { { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { sortOrder: SortOrder.ASC }, ); - checkSimple(searchResult).toEqual(members); + expect(searchResult).toEqual(members); // order and query all extra data searchResult = await client.geosearch( @@ -5395,7 +5356,7 @@ export function runBaseTests(config: { withHash: true, }, ); - checkSimple(searchResult).toEqual(expectedResult); + expect(searchResult).toEqual(expectedResult); // order, query and limit by 1 searchResult = await client.geosearch( @@ -5410,7 +5371,7 @@ export function runBaseTests(config: { count: 1, }, ); - checkSimple(searchResult).toEqual(expectedResult.slice(0, 1)); + expect(searchResult).toEqual(expectedResult.slice(0, 1)); // test search by box, unit: meters, from member, with distance const meters = 400 * 1000; @@ -5424,7 +5385,7 @@ export function runBaseTests(config: { sortOrder: SortOrder.DESC, }, ); - checkSimple(searchResult).toEqual([ + expect(searchResult).toEqual([ ["edge2", [236529.1799]], ["Palermo", [166274.1516]], ["Catania", [0.0]], @@ -5444,7 +5405,7 @@ export function runBaseTests(config: { count: 2, }, ); - checkSimple(searchResult).toEqual([ + expect(searchResult).toEqual([ ["Palermo", [3479099956230698]], ["edge1", [3479273021651468]], ]); @@ -5457,9 +5418,7 @@ export function runBaseTests(config: { { width: miles, height: miles, unit: GeoUnit.MILES }, { count: 1, isAny: true }, ); - expect(members.map((m) => Buffer.from(m))).toContainEqual( - searchResult[0], - ); + expect(members).toContainEqual(searchResult[0]); // test search by radius, units: feet, from member const feetRadius = 200 * 3280.8399; @@ -5469,7 +5428,7 @@ export function runBaseTests(config: { { radius: feetRadius, unit: GeoUnit.FEET }, { sortOrder: SortOrder.ASC }, ); - checkSimple(searchResult).toEqual(["Catania", "Palermo"]); + expect(searchResult).toEqual(["Catania", "Palermo"]); // Test search by radius, unit: meters, from member const metersRadius = 200 * 1000; @@ -5479,7 +5438,7 @@ export function runBaseTests(config: { { radius: metersRadius, unit: GeoUnit.METERS }, { sortOrder: SortOrder.DESC }, ); - checkSimple(searchResult).toEqual(["Palermo", "Catania"]); + expect(searchResult).toEqual(["Palermo", "Catania"]); searchResult = await client.geosearch( key, @@ -5490,7 +5449,7 @@ export function runBaseTests(config: { withHash: true, }, ); - checkSimple(searchResult).toEqual([ + expect(searchResult).toEqual([ ["Palermo", [3479099956230698]], ["Catania", [3479447370796909]], ]); @@ -5502,7 +5461,7 @@ export function runBaseTests(config: { { radius: 175, unit: GeoUnit.MILES }, { sortOrder: SortOrder.DESC }, ); - checkSimple(searchResult).toEqual([ + expect(searchResult).toEqual([ "edge1", "edge2", "Palermo", @@ -5522,7 +5481,7 @@ export function runBaseTests(config: { withDist: true, }, ); - checkSimple(searchResult).toEqual(expectedResult.slice(0, 2)); + expect(searchResult).toEqual(expectedResult.slice(0, 2)); // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 searchResult = await client.geosearch( @@ -5538,9 +5497,7 @@ export function runBaseTests(config: { withHash: true, }, ); - expect(members.map((m) => Buffer.from(m))).toContainEqual( - searchResult[0][0], - ); + expect(members).toContainEqual(searchResult[0][0]); // no members within the area searchResult = await client.geosearch( @@ -5599,10 +5556,10 @@ export function runBaseTests(config: { 2, ); - checkSimple( + expect( await client.zmpop([key1, key2], ScoreFilter.MAX), ).toEqual([key1, { b1: 2 }]); - checkSimple( + expect( await client.zmpop([key2, key1], ScoreFilter.MAX, 10), ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); @@ -5697,10 +5654,10 @@ export function runBaseTests(config: { 2, ); - checkSimple( + expect( await client.bzmpop([key1, key2], ScoreFilter.MAX, 0.1), ).toEqual([key1, { b1: 2 }]); - checkSimple( + expect( await client.bzmpop([key2, key1], ScoreFilter.MAX, 0.1, 10), ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); @@ -5876,7 +5833,7 @@ export function runBaseTests(config: { const randmember = await client.zrandmember(key1); if (randmember !== null) { - checkSimple(randmember in elements).toEqual(true); + expect(elements.includes(randmember)).toEqual(true); } // non existing key should return null @@ -6028,7 +5985,7 @@ export function runCommonTests(config: { const value = "שלום hello 汉字"; await client.set(key, value); const result = await client.get(key); - checkSimple(result).toEqual(value); + expect(result).toEqual(value); }); }, config.timeout, @@ -6040,7 +5997,7 @@ export function runCommonTests(config: { await runTest(async (client: Client) => { const result = await client.get(uuidv4()); - checkSimple(result).toEqual(null); + expect(result).toEqual(null); }); }, config.timeout, @@ -6054,7 +6011,7 @@ export function runCommonTests(config: { await client.set(key, ""); const result = await client.get(key); - checkSimple(result).toEqual(""); + expect(result).toEqual(""); }); }, config.timeout, @@ -6081,7 +6038,7 @@ export function runCommonTests(config: { await client.set(key, value); const result = await client.get(key); - checkSimple(result).toEqual(value); + expect(result).toEqual(value); }); }, config.timeout, @@ -6096,7 +6053,7 @@ export function runCommonTests(config: { await GetAndSetRandomValue(client); } else { const result = await client.get(uuidv4()); - checkSimple(result).toEqual(null); + expect(result).toEqual(null); } }; diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 671f320520..6b317bd5e1 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -380,15 +380,15 @@ export function checkFunctionListResponse( const flags = ( functionInfo["flags"] as unknown as Buffer[] ).map((f) => f.toString()); - checkSimple(functionInfo["description"]).toEqual( + expect(functionInfo["description"]).toEqual( functionDescriptions.get(name), ); - checkSimple(flags).toEqual(functionFlags.get(name)); + expect(flags).toEqual(functionFlags.get(name)); } if (libCode) { - checkSimple(lib["library_code"]).toEqual(libCode); + expect(lib["library_code"]).toEqual(libCode); } break; From de6cb12f4b07683393b6404e7608467247d69eed Mon Sep 17 00:00:00 2001 From: ort-bot Date: Wed, 31 Jul 2024 00:58:52 +0000 Subject: [PATCH 099/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 54 ++++++++++++++++++++++++++- java/THIRD_PARTY_LICENSES_JAVA | 54 ++++++++++++++++++++++++++- node/THIRD_PARTY_LICENSES_NODE | 2 +- python/THIRD_PARTY_LICENSES_PYTHON | 56 +++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 5 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index d092cb25c5..04a0d8e1a8 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -3451,6 +3451,58 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: byteorder:1.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + Package: bytes:1.6.1 The following copyrights and licenses were found in the source code of this package: @@ -17294,7 +17346,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.17 +Package: ppv-lite86:0.2.19 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 39d0662505..2878da8a61 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -3451,6 +3451,58 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: byteorder:1.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + Package: bytes:1.6.1 The following copyrights and licenses were found in the source code of this package: @@ -18189,7 +18241,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.17 +Package: ppv-lite86:0.2.19 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 51b92788b3..f6e379af41 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -18003,7 +18003,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.17 +Package: ppv-lite86:0.2.19 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 0f992994e4..3b89a9bf00 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -3451,6 +3451,58 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: byteorder:1.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + Package: bytes:1.6.1 The following copyrights and licenses were found in the source code of this package: @@ -18214,7 +18266,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.17 +Package: ppv-lite86:0.2.19 The following copyrights and licenses were found in the source code of this package: @@ -25263,7 +25315,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: target-lexicon:0.12.15 +Package: target-lexicon:0.12.16 The following copyrights and licenses were found in the source code of this package: From 55b776be6d73b5fdbd65872d72735934f69f375c Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:35:27 +0300 Subject: [PATCH 100/236] Node: temporary fix CI (#2060) Signed-off-by: Shoham Elias --- node/tests/SharedTests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 2ba3694c25..0b884e9f26 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -45,6 +45,7 @@ import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { Client, GetAndSetRandomValue, + checkSimple, compareMaps, getFirstResult, intoString, From b9e7cefa0591292e2550e48a80355c2ab52b9776 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:15:57 +0300 Subject: [PATCH 101/236] Python: add PubSub commands (#2043) --------- Signed-off-by: Shoham Elias --- glide-core/src/client/value_conversion.rs | 4 + glide-core/src/protobuf/command_request.proto | 5 + glide-core/src/request_type.rs | 15 + .../glide/async_commands/cluster_commands.py | 62 +++ python/python/glide/async_commands/core.py | 83 +++ .../glide/async_commands/transaction.py | 96 ++++ python/python/tests/test_pubsub.py | 491 +++++++++++++++++- python/python/tests/test_transaction.py | 14 + 8 files changed, 769 insertions(+), 1 deletion(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 7a9ceced96..95021054c3 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -1265,6 +1265,10 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { }) } } + b"PUBSUB NUMSUB" | b"PUBSUB SHARDNUMSUB" => Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }), _ => None, } } diff --git a/glide-core/src/protobuf/command_request.proto b/glide-core/src/protobuf/command_request.proto index 96d3f7ae43..bd122f12a1 100644 --- a/glide-core/src/protobuf/command_request.proto +++ b/glide-core/src/protobuf/command_request.proto @@ -248,6 +248,11 @@ enum RequestType { Scan = 206; Wait = 208; XClaim = 209; + PubSubChannels = 210; + PubSubNumPat = 211; + PubSubNumSub = 212; + PubSubSChannels = 213; + PubSubSNumSub = 214; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index cd511f73a2..e574251e27 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -218,6 +218,11 @@ pub enum RequestType { Scan = 206, Wait = 208, XClaim = 209, + PubSubChannels = 210, + PubSubNumPat = 211, + PubSubNumSub = 212, + PubSubSChannels = 213, + PubSubSNumSub = 214, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -439,6 +444,11 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Wait => RequestType::Wait, ProtobufRequestType::XClaim => RequestType::XClaim, ProtobufRequestType::Scan => RequestType::Scan, + ProtobufRequestType::PubSubChannels => RequestType::PubSubChannels, + ProtobufRequestType::PubSubNumSub => RequestType::PubSubNumSub, + ProtobufRequestType::PubSubNumPat => RequestType::PubSubNumPat, + ProtobufRequestType::PubSubSChannels => RequestType::PubSubSChannels, + ProtobufRequestType::PubSubSNumSub => RequestType::PubSubSNumSub, } } } @@ -658,6 +668,11 @@ impl RequestType { RequestType::Wait => Some(cmd("WAIT")), RequestType::XClaim => Some(cmd("XCLAIM")), RequestType::Scan => Some(cmd("SCAN")), + RequestType::PubSubChannels => Some(get_two_word_command("PUBSUB", "CHANNELS")), + RequestType::PubSubNumSub => Some(get_two_word_command("PUBSUB", "NUMSUB")), + RequestType::PubSubNumPat => Some(get_two_word_command("PUBSUB", "NUMPAT")), + RequestType::PubSubSChannels => Some(get_two_word_command("PUBSUB", "SHARDCHANNELS")), + RequestType::PubSubSNumSub => Some(get_two_word_command("PUBSUB", "SHARDNUMSUB")), } } } diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index a0044c3e92..f59cb4662d 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -920,6 +920,68 @@ async def publish( ) return cast(int, result) + async def pubsub_shardchannels( + self, pattern: Optional[TEncodable] = None + ) -> List[bytes]: + """ + Lists the currently active shard channels. + The command is routed to all nodes, and aggregates the response to a single array. + + See https://valkey.io/commands/pubsub-shardchannels for more details. + + Args: + pattern (Optional[TEncodable]): A glob-style pattern to match active shard channels. + If not provided, all active shard channels are returned. + + Returns: + List[bytes]: A list of currently active shard channels matching the given pattern. + If no pattern is specified, all active shard channels are returned. + + Examples: + >>> await client.pubsub_shardchannels() + [b'channel1', b'channel2'] + + >>> await client.pubsub_shardchannels("channel*") + [b'channel1', b'channel2'] + """ + command_args = [pattern] if pattern is not None else [] + return cast( + List[bytes], + await self._execute_command(RequestType.PubSubSChannels, command_args), + ) + + async def pubsub_shardnumsub( + self, channels: Optional[List[TEncodable]] = None + ) -> Mapping[bytes, int]: + """ + Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. + + Note that it is valid to call this command without channels. In this case, it will just return an empty map. + The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + + See https://valkey.io/commands/pubsub-shardnumsub for more details. + + Args: + channels (Optional[List[TEncodable]]): The list of shard channels to query for the number of subscribers. + If not provided, returns an empty map. + + Returns: + Mapping[bytes, int]: A map where keys are the shard channel names and values are the number of subscribers. + + Examples: + >>> await client.pubsub_shardnumsub(["channel1", "channel2"]) + {b'channel1': 3, b'channel2': 5} + + >>> await client.pubsub_shardnumsub() + {} + """ + return cast( + Mapping[bytes, int], + await self._execute_command( + RequestType.PubSubSNumSub, channels if channels else [] + ), + ) + async def flushall( self, flush_mode: Optional[FlushMode] = None, route: Optional[Route] = None ) -> TOK: diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index e20f07bbc2..a8c0d63062 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -6628,3 +6628,86 @@ async def lpos( Union[int, List[int], None], await self._execute_command(RequestType.LPos, args), ) + + async def pubsub_channels( + self, pattern: Optional[TEncodable] = None + ) -> List[bytes]: + """ + Lists the currently active channels. + The command is routed to all nodes, and aggregates the response to a single array. + + See https://valkey.io/commands/pubsub-channels for more details. + + Args: + pattern (Optional[TEncodable]): A glob-style pattern to match active channels. + If not provided, all active channels are returned. + + Returns: + List[bytes]: A list of currently active channels matching the given pattern. + If no pattern is specified, all active channels are returned. + + Examples: + >>> await client.pubsub_channels() + [b"channel1", b"channel2"] + + >>> await client.pubsub_channels("news.*") + [b"news.sports", "news.weather"] + """ + + return cast( + List[bytes], + await self._execute_command( + RequestType.PubSubChannels, [pattern] if pattern else [] + ), + ) + + async def pubsub_numpat(self) -> int: + """ + Returns the number of unique patterns that are subscribed to by clients. + + Note: This is the total number of unique patterns all the clients are subscribed to, + not the count of clients subscribed to patterns. + The command is routed to all nodes, and aggregates the response the sum of all pattern subscriptions. + + See https://valkey.io/commands/pubsub-numpat for more details. + + Returns: + int: The number of unique patterns. + + Examples: + >>> await client.pubsub_numpat() + 3 + """ + return cast(int, await self._execute_command(RequestType.PubSubNumPat, [])) + + async def pubsub_numsub( + self, channels: Optional[List[TEncodable]] = None + ) -> Mapping[bytes, int]: + """ + Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + + Note that it is valid to call this command without channels. In this case, it will just return an empty map. + The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + + See https://valkey.io/commands/pubsub-numsub for more details. + + Args: + channels (Optional[List[TEncodable]]): The list of channels to query for the number of subscribers. + If not provided, returns an empty map. + + Returns: + Mapping[bytes, int]: A map where keys are the channel names and values are the number of subscribers. + + Examples: + >>> await client.pubsub_numsub(["channel1", "channel2"]) + {b'channel1': 3, b'channel2': 5} + + >>> await client.pubsub_numsub() + {} + """ + return cast( + Mapping[bytes, int], + await self._execute_command( + RequestType.PubSubNumSub, channels if channels else [] + ), + ) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 6a95c2f081..1573d51c03 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -4780,6 +4780,62 @@ def xclaim_just_id( return self.append_command(RequestType.XClaim, args) + def pubsub_channels( + self: TTransaction, pattern: Optional[TEncodable] = None + ) -> TTransaction: + """ + Lists the currently active channels. + + See https://valkey.io/commands/pubsub-channels for details. + + Args: + pattern (Optional[TEncodable]): A glob-style pattern to match active channels. + If not provided, all active channels are returned. + + Command response: + List[bytes]: A list of currently active channels matching the given pattern. + If no pattern is specified, all active channels are returned. + """ + + return self.append_command( + RequestType.PubSubChannels, [pattern] if pattern else [] + ) + + def pubsub_numpat(self: TTransaction) -> TTransaction: + """ + Returns the number of unique patterns that are subscribed to by clients. + + Note: This is the total number of unique patterns all the clients are subscribed to, + not the count of clients subscribed to patterns. + + See https://valkey.io/commands/pubsub-numpat for details. + + Command response: + int: The number of unique patterns. + """ + return self.append_command(RequestType.PubSubNumPat, []) + + def pubsub_numsub( + self: TTransaction, channels: Optional[List[TEncodable]] = None + ) -> TTransaction: + """ + Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + + Note that it is valid to call this command without channels. In this case, it will just return an empty map. + + See https://valkey.io/commands/pubsub-numsub for details. + + Args: + channels (Optional[List[str]]): The list of channels to query for the number of subscribers. + If not provided, returns an empty map. + + Command response: + Mapping[bytes, int]: A map where keys are the channel names and values are the number of subscribers. + """ + return self.append_command( + RequestType.PubSubNumSub, channels if channels else [] + ) + class Transaction(BaseTransaction): """ @@ -5172,4 +5228,44 @@ def publish( RequestType.SPublish if sharded else RequestType.Publish, [channel, message] ) + def pubsub_shardchannels( + self, pattern: Optional[TEncodable] = None + ) -> "ClusterTransaction": + """ + Lists the currently active shard channels. + + See https://valkey.io/commands/pubsub-shardchannels for details. + + Args: + pattern (Optional[TEncodable]): A glob-style pattern to match active shard channels. + If not provided, all active shard channels are returned. + + Command response: + List[bytes]: A list of currently active shard channels matching the given pattern. + If no pattern is specified, all active shard channels are returned. + """ + command_args = [pattern] if pattern is not None else [] + return self.append_command(RequestType.PubSubSChannels, command_args) + + def pubsub_shardnumsub( + self, channels: Optional[List[TEncodable]] = None + ) -> "ClusterTransaction": + """ + Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. + + Note that it is valid to call this command without channels. In this case, it will just return an empty map. + + See https://valkey.io/commands/pubsub-shardnumsub for details. + + Args: + channels (Optional[List[str]]): The list of shard channels to query for the number of subscribers. + If not provided, returns an empty map. + + Command response: + Mapping[bytes, int]: A map where keys are the shard channel names and values are the number of subscribers. + """ + return self.append_command( + RequestType.PubSubSNumSub, channels if channels else [] + ) + # TODO: add all CLUSTER commands diff --git a/python/python/tests/test_pubsub.py b/python/python/tests/test_pubsub.py index 23b5bb6709..79735d4282 100644 --- a/python/python/tests/test_pubsub.py +++ b/python/python/tests/test_pubsub.py @@ -13,7 +13,7 @@ GlideClusterClientConfiguration, ProtocolVersion, ) -from glide.constants import OK +from glide.constants import OK, TEncodable from glide.exceptions import ConfigurationError from glide.glide_client import BaseClient, GlideClient, GlideClusterClient, TGlideClient from tests.conftest import create_client @@ -2257,3 +2257,492 @@ async def test_pubsub_context_with_no_callback_raise_error( with pytest.raises(ConfigurationError): await create_two_clients_with_pubsub(request, cluster_mode, pub_sub_exact) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_channels(self, request, cluster_mode: bool): + """ + Tests the pubsub_channels command functionality. + + This test verifies that the pubsub_channels command correctly returns + the active channels matching a specified pattern. + """ + client1, client2, client = None, None, None + try: + channel1 = "test_channel1" + channel2 = "test_channel2" + channel3 = "some_channel3" + pattern = "test_*" + + client = await create_client(request, cluster_mode) + # Assert no channels exists yet + assert await client.pubsub_channels() == [] + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + channel1, + channel2, + channel3, + } + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: { + channel1, + channel2, + channel3, + } + }, + ) + + channel1_bytes = channel1.encode() + channel2_bytes = channel2.encode() + channel3_bytes = channel3.encode() + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + # Test pubsub_channels without pattern + channels = await client2.pubsub_channels() + assert set(channels) == {channel1_bytes, channel2_bytes, channel3_bytes} + + # Test pubsub_channels with pattern + channels_with_pattern = await client2.pubsub_channels(pattern) + assert set(channels_with_pattern) == {channel1_bytes, channel2_bytes} + + # Test with non-matching pattern + non_matching_channels = await client2.pubsub_channels("non_matching_*") + assert len(non_matching_channels) == 0 + + finally: + await client_cleanup(client1, pub_sub if cluster_mode else None) + await client_cleanup(client2, None) + await client_cleanup(client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_numpat(self, request, cluster_mode: bool): + """ + Tests the pubsub_numpat command functionality. + + This test verifies that the pubsub_numpat command correctly returns + the number of unique patterns that are subscribed to by clients. + """ + client1, client2, client = None, None, None + try: + pattern1 = "test_*" + pattern2 = "another_*" + + # Create a client and check initial number of patterns + client = await create_client(request, cluster_mode) + assert await client.pubsub_numpat() == 0 + + # Set up subscriptions with patterns + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + pattern1, + pattern2, + } + }, + { + GlideClientConfiguration.PubSubChannelModes.Pattern: { + pattern1, + pattern2, + } + }, + ) + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + # Test pubsub_numpat + num_patterns = await client2.pubsub_numpat() + assert num_patterns == 2 + + finally: + await client_cleanup(client1, pub_sub if cluster_mode else None) + await client_cleanup(client2, None) + await client_cleanup(client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_numsub(self, request, cluster_mode: bool): + """ + Tests the pubsub_numsub command functionality. + + This test verifies that the pubsub_numsub command correctly returns + the number of subscribers for specified channels. + """ + client1, client2, client3, client4, client = None, None, None, None, None + try: + channel1 = "test_channel1" + channel2 = "test_channel2" + channel3 = "test_channel3" + channel4 = "test_channel4" + + # Set up subscriptions + pub_sub1 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + channel1, + channel2, + channel3, + } + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: { + channel1, + channel2, + channel3, + } + }, + ) + pub_sub2 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + channel2, + channel3, + } + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: { + channel2, + channel3, + } + }, + ) + pub_sub3 = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel3}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel3}}, + ) + + channel1_bytes = channel1.encode() + channel2_bytes = channel2.encode() + channel3_bytes = channel3.encode() + channel4_bytes = channel4.encode() + + # Create a client and check initial subscribers + client = await create_client(request, cluster_mode) + assert await client.pubsub_numsub([channel1, channel2, channel3]) == { + channel1_bytes: 0, + channel2_bytes: 0, + channel3_bytes: 0, + } + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub1, pub_sub2 + ) + client3, client4 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub3 + ) + + # Test pubsub_numsub + subscribers = await client2.pubsub_numsub( + [channel1_bytes, channel2_bytes, channel3_bytes, channel4_bytes] + ) + assert subscribers == { + channel1_bytes: 1, + channel2_bytes: 2, + channel3_bytes: 3, + channel4_bytes: 0, + } + + # Test pubsub_numsub with no channels + empty_subscribers = await client2.pubsub_numsub() + assert empty_subscribers == {} + + finally: + await client_cleanup(client1, pub_sub1 if cluster_mode else None) + await client_cleanup(client2, pub_sub2 if cluster_mode else None) + await client_cleanup(client3, pub_sub3 if cluster_mode else None) + await client_cleanup(client4, None) + await client_cleanup(client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_shardchannels(self, request, cluster_mode: bool): + """ + Tests the pubsub_shardchannels command functionality. + + This test verifies that the pubsub_shardchannels command correctly returns + the active sharded channels matching a specified pattern. + """ + client1, client2, client = None, None, None + try: + channel1 = "test_shardchannel1" + channel2 = "test_shardchannel2" + channel3 = "some_shardchannel3" + pattern = "test_*" + + client = await create_client(request, cluster_mode) + assert type(client) == GlideClusterClient + # Assert no sharded channels exist yet + assert await client.pubsub_shardchannels() == [] + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + channel1, + channel2, + channel3, + } + }, + {}, # Empty dict for non-cluster mode as sharded channels are not supported + ) + + channel1_bytes = channel1.encode() + channel2_bytes = channel2.encode() + channel3_bytes = channel3.encode() + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(client1, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + assert type(client2) == GlideClusterClient + + # Test pubsub_shardchannels without pattern + channels = await client2.pubsub_shardchannels() + assert set(channels) == {channel1_bytes, channel2_bytes, channel3_bytes} + + # Test pubsub_shardchannels with pattern + channels_with_pattern = await client2.pubsub_shardchannels(pattern) + assert set(channels_with_pattern) == {channel1_bytes, channel2_bytes} + + # Test with non-matching pattern + assert await client2.pubsub_shardchannels("non_matching_*") == [] + + finally: + await client_cleanup(client1, pub_sub if cluster_mode else None) + await client_cleanup(client2, None) + await client_cleanup(client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_shardnumsub(self, request, cluster_mode: bool): + """ + Tests the pubsub_shardnumsub command functionality. + + This test verifies that the pubsub_shardnumsub command correctly returns + the number of subscribers for specified sharded channels. + """ + client1, client2, client3, client4, client = None, None, None, None, None + try: + channel1 = "test_shardchannel1" + channel2 = "test_shardchannel2" + channel3 = "test_shardchannel3" + channel4 = "test_shardchannel4" + + # Set up subscriptions + pub_sub1 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + channel1, + channel2, + channel3, + } + }, + {}, + ) + pub_sub2 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + channel2, + channel3, + } + }, + {}, + ) + pub_sub3 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + channel3 + } + }, + {}, + ) + + channel1_bytes = channel1.encode() + channel2_bytes = channel2.encode() + channel3_bytes = channel3.encode() + channel4_bytes = channel4.encode() + + # Create a client and check initial subscribers + client = await create_client(request, cluster_mode) + assert type(client) == GlideClusterClient + assert await client.pubsub_shardnumsub([channel1, channel2, channel3]) == { + channel1_bytes: 0, + channel2_bytes: 0, + channel3_bytes: 0, + } + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub1, pub_sub2 + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(client1, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + client3, client4 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub3 + ) + + assert type(client4) == GlideClusterClient + + # Test pubsub_shardnumsub + subscribers = await client4.pubsub_shardnumsub( + [channel1, channel2, channel3, channel4] + ) + assert subscribers == { + channel1_bytes: 1, + channel2_bytes: 2, + channel3_bytes: 3, + channel4_bytes: 0, + } + + # Test pubsub_shardnumsub with no channels + empty_subscribers = await client4.pubsub_shardnumsub() + assert empty_subscribers == {} + + finally: + await client_cleanup(client1, pub_sub1 if cluster_mode else None) + await client_cleanup(client2, pub_sub2 if cluster_mode else None) + await client_cleanup(client3, pub_sub3 if cluster_mode else None) + await client_cleanup(client4, None) + await client_cleanup(client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_channels_and_shardchannels_separation( + self, request, cluster_mode: bool + ): + """ + Tests that pubsub_channels doesn't return sharded channels and pubsub_shardchannels + doesn't return regular channels. + """ + client1, client2 = None, None + try: + regular_channel = "regular_channel" + shard_channel = "shard_channel" + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + regular_channel + }, + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + shard_channel + }, + }, + {GlideClientConfiguration.PubSubChannelModes.Exact: {regular_channel}}, + ) + + regular_channel_bytes, shard_channel_bytes = ( + regular_channel.encode(), + shard_channel.encode(), + ) + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(client1, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + assert type(client2) == GlideClusterClient + # Test pubsub_channels + assert await client2.pubsub_channels() == [regular_channel_bytes] + + # Test pubsub_shardchannels + assert await client2.pubsub_shardchannels() == [shard_channel_bytes] + + finally: + await client_cleanup(client1, pub_sub if cluster_mode else None) + await client_cleanup(client2, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_numsub_and_shardnumsub_separation( + self, request, cluster_mode: bool + ): + """ + Tests that pubsub_numsub doesn't count sharded channel subscribers and pubsub_shardnumsub + doesn't count regular channel subscribers. + """ + client1, client2 = None, None + try: + regular_channel = "regular_channel" + shard_channel = "shard_channel" + + pub_sub1 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + regular_channel + }, + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + shard_channel + }, + }, + {}, + ) + pub_sub2 = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + regular_channel + }, + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + shard_channel + }, + }, + {}, + ) + + regular_channel_bytes: bytes = regular_channel.encode() + shard_channel_bytes: bytes = shard_channel.encode() + + client1, client2 = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub1, pub_sub2 + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(client1, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + assert type(client2) == GlideClusterClient + + # Test pubsub_numsub + regular_subscribers = await client2.pubsub_numsub( + [regular_channel_bytes, shard_channel_bytes] + ) + + assert regular_subscribers == { + regular_channel_bytes: 2, + shard_channel_bytes: 0, + } + + # Test pubsub_shardnumsub + shard_subscribers = await client2.pubsub_shardnumsub( + [regular_channel_bytes, shard_channel_bytes] + ) + + assert shard_subscribers == { + regular_channel_bytes: 0, + shard_channel_bytes: 2, + } + + finally: + await client_cleanup(client1, pub_sub1 if cluster_mode else None) + await client_cleanup(client2, pub_sub2 if cluster_mode else None) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 2b1293b943..9d8f09f865 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -772,6 +772,13 @@ async def transaction_test( transaction.lcs_idx(key23, key24, with_match_len=True) args.append({b"matches": [[[4, 7], [5, 8], 4], [[1, 3], [0, 2], 3]], b"len": 7}) + transaction.pubsub_channels(pattern="*") + args.append([]) + transaction.pubsub_numpat() + args.append(0) + transaction.pubsub_numsub() + args.append({}) + return args @@ -882,6 +889,13 @@ async def test_cluster_transaction(self, glide_client: GlideClusterClient): else: transaction.publish("test_message", keyslot, True) expected = await transaction_test(transaction, keyslot, glide_client) + + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.pubsub_shardchannels() + expected.append([]) + transaction.pubsub_shardnumsub() + expected.append({}) + result = await glide_client.exec(transaction) assert isinstance(result, list) assert isinstance(result[0], bytes) From 33e30e9f714f35dce74aad8571fd6796979ff505 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 09:53:12 -0700 Subject: [PATCH 102/236] Node: Fix tests. (#2056) * Fix tests. Signed-off-by: Yury-Fridlyand --- node/src/BaseClient.ts | 6 ++-- node/tests/PubSub.test.ts | 22 ++++++++++++-- node/tests/RedisClient.test.ts | 3 +- node/tests/SharedTests.ts | 55 +++++++++++++++++----------------- node/tests/TestUtilities.ts | 33 +++++++++++++------- 5 files changed, 74 insertions(+), 45 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 941bf45fe1..0dacbff526 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -185,10 +185,11 @@ import { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ type PromiseFunction = (value?: any) => void; type ErrorFunction = (error: RedisError) => void; -export type ReturnTypeMap = { [key: string]: ReturnType }; +export type ReturnTypeRecord = { [key: string]: ReturnType }; +export type ReturnTypeMap = Map; export type ReturnTypeAttribute = { value: ReturnType; - attributes: ReturnTypeMap; + attributes: ReturnTypeRecord; }; export enum ProtocolVersion { /** Use RESP2 to communicate with the server nodes. */ @@ -205,6 +206,7 @@ export type ReturnType = | bigint | Buffer | Set + | ReturnTypeRecord | ReturnTypeMap | ReturnTypeAttribute | ReturnType[]; diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts index 1ced6c74d5..5545a94a46 100644 --- a/node/tests/PubSub.test.ts +++ b/node/tests/PubSub.test.ts @@ -23,7 +23,11 @@ import { TimeoutError, } from ".."; import RedisCluster from "../../utils/TestUtils"; -import { flushAndCloseClient } from "./TestUtilities"; +import { + flushAndCloseClient, + parseCommandLineArgs, + parseEndpoints, +} from "./TestUtilities"; export type TGlideClient = GlideClient | GlideClusterClient; @@ -41,8 +45,20 @@ describe("PubSub", () => { let cmeCluster: RedisCluster; let cmdCluster: RedisCluster; beforeAll(async () => { - cmdCluster = await RedisCluster.createCluster(false, 1, 1); - cmeCluster = await RedisCluster.createCluster(true, 3, 1); + const standaloneAddresses = + parseCommandLineArgs()["standalone-endpoints"]; + const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; + // Connect to cluster or create a new one based on the parsed addresses + cmdCluster = standaloneAddresses + ? await RedisCluster.initFromExistingCluster( + parseEndpoints(standaloneAddresses), + ) + : await RedisCluster.createCluster(false, 1, 1); + cmeCluster = clusterAddresses + ? await RedisCluster.initFromExistingCluster( + parseEndpoints(clusterAddresses), + ) + : await RedisCluster.createCluster(true, 3, 1); }, 40000); afterEach(async () => { await flushAndCloseClient(false, cmdCluster.getAddresses()); diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index ba728371ef..566eff9adc 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -12,7 +12,7 @@ import { } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; -import { GlideClient, ProtocolVersion, Transaction } from ".."; +import { GlideClient, ProtocolVersion, Transaction, ListDirection } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; @@ -29,7 +29,6 @@ import { transactionTest, validateTransactionResponse, } from "./TestUtilities"; -import { ListDirection } from ".."; /* eslint-disable @typescript-eslint/no-var-requires */ diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0b884e9f26..1f62c7f1c8 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -45,10 +45,8 @@ import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { Client, GetAndSetRandomValue, - checkSimple, compareMaps, getFirstResult, - intoString, } from "./TestUtilities"; export type BaseClient = GlideClient | GlideClusterClient; @@ -284,12 +282,21 @@ export function runBaseTests(config: { /// we execute set and info so the commandstats will show `cmdstat_set::calls` greater than 1 /// after the configResetStat call we initiate an info command and the the commandstats won't contain `cmdstat_set`. await client.set("foo", "bar"); - const oldResult = await client.info([InfoOptions.Commandstats]); - const oldResultAsString = intoString(oldResult); - expect(oldResultAsString).toContain("cmdstat_set"); + const oldResult = + client instanceof GlideClient + ? await client.info([InfoOptions.Commandstats]) + : Object.values( + await client.info([InfoOptions.Commandstats]), + ).join(); + expect(oldResult).toContain("cmdstat_set"); expect(await client.configResetStat()).toEqual("OK"); - const result = await client.info([InfoOptions.Commandstats]); + const result = + client instanceof GlideClient + ? await client.info([InfoOptions.Commandstats]) + : Object.values( + await client.info([InfoOptions.Commandstats]), + ).join(); expect(result).not.toContain("cmdstat_set"); }, protocol); }, @@ -338,14 +345,14 @@ export function runBaseTests(config: { expect(await client.msetnx(keyValueMap1)).toEqual(true); - checkSimple( - await client.mget([key1, key2, nonExistingKey]), - ).toEqual([value, value, null]); + expect(await client.mget([key1, key2, nonExistingKey])).toEqual( + [value, value, null], + ); expect(await client.msetnx(keyValueMap2)).toEqual(false); expect(await client.get(key3)).toEqual(null); - checkSimple(await client.get(key2)).toEqual(value); + expect(await client.get(key2)).toEqual(value); // empty map and RequestError is thrown const emptyMap = {}; @@ -1694,7 +1701,7 @@ export function runBaseTests(config: { expect(await client.lpush(key2, lpushArgs2)).toEqual(2); // Move from LEFT to LEFT with blocking - checkSimple( + expect( await client.blmove( key1, key2, @@ -1705,7 +1712,7 @@ export function runBaseTests(config: { ).toEqual("1"); // Move from LEFT to RIGHT with blocking - checkSimple( + expect( await client.blmove( key1, key2, @@ -1715,16 +1722,16 @@ export function runBaseTests(config: { ), ).toEqual("2"); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + expect(await client.lrange(key2, 0, -1)).toEqual([ "1", "3", "4", "2", ]); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([]); + expect(await client.lrange(key1, 0, -1)).toEqual([]); // Move from RIGHT to LEFT non-existing destination with blocking - checkSimple( + expect( await client.blmove( key2, key1, @@ -1734,15 +1741,15 @@ export function runBaseTests(config: { ), ).toEqual("2"); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + expect(await client.lrange(key2, 0, -1)).toEqual([ "1", "3", "4", ]); - checkSimple(await client.lrange(key1, 0, -1)).toEqual(["2"]); + expect(await client.lrange(key1, 0, -1)).toEqual(["2"]); // Move from RIGHT to RIGHT with blocking - checkSimple( + expect( await client.blmove( key2, key1, @@ -1752,14 +1759,8 @@ export function runBaseTests(config: { ), ).toEqual("4"); - checkSimple(await client.lrange(key2, 0, -1)).toEqual([ - "1", - "3", - ]); - checkSimple(await client.lrange(key1, 0, -1)).toEqual([ - "2", - "4", - ]); + expect(await client.lrange(key2, 0, -1)).toEqual(["1", "3"]); + expect(await client.lrange(key1, 0, -1)).toEqual(["2", "4"]); // Non-existing source key with blocking expect( @@ -1774,7 +1775,7 @@ export function runBaseTests(config: { // Non-list source key with blocking const key3 = "{key}-3" + uuidv4(); - checkSimple(await client.set(key3, "value")).toEqual("OK"); + expect(await client.set(key3, "value")).toEqual("OK"); await expect( client.blmove( key3, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a576b15103..f7f189fc74 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -27,6 +27,7 @@ import { ListDirection, ProtocolVersion, ReturnType, + ReturnTypeMap, ScoreFilter, SignedEncoding, SortOrder, @@ -374,12 +375,8 @@ export function checkFunctionListResponse( string, string | string[] >; - const name = ( - functionInfo["name"] as unknown as Buffer - ).toString(); // not a string - suprise - const flags = ( - functionInfo["flags"] as unknown as Buffer[] - ).map((f) => f.toString()); + const name = functionInfo["name"] as string; + const flags = functionInfo["flags"] as string[]; expect(functionInfo["description"]).toEqual( functionDescriptions.get(name), ); @@ -412,9 +409,23 @@ export function validateTransactionResponse( for (let i = 0; i < expectedResponseData.length; i++) { const [testName, expectedResponse] = expectedResponseData[i]; - if (intoString(response?.[i]) != intoString(expectedResponse)) { + try { + expect(response?.[i]).toEqual(expectedResponse); + } catch (e) { + const expected = + expectedResponse instanceof Map + ? JSON.stringify(Array.from(expectedResponse.entries())) + : JSON.stringify(expectedResponse); + const actual = + response?.[i] instanceof Map + ? JSON.stringify( + Array.from( + (response?.[i] as ReturnTypeMap)?.entries(), + ), + ) + : JSON.stringify(response?.[i]); failedChecks.push( - `${testName} failed, expected <${JSON.stringify(expectedResponse)}>, actual <${JSON.stringify(response?.[i])}>`, + `${testName} failed, expected <${expected}>, actual <${actual}>`, ); } } @@ -634,7 +645,7 @@ export async function transactionTest( baseTransaction.smismember(key7, ["bar", "foo", "baz"]); responseData.push([ 'smismember(key7, ["bar", "foo", "baz"])', - [true, true, false], + [true, false, false], ]); } @@ -904,11 +915,11 @@ export async function transactionTest( baseTransaction.zrandmember(key21); responseData.push(["zrandmember(key21)", "one"]); baseTransaction.zrandmemberWithCount(key21, 1); - responseData.push(["zrandmemberWithCountWithScores(key21, 1)", "one"]); + responseData.push(["zrandmemberWithCount(key21, 1)", ["one"]]); baseTransaction.zrandmemberWithCountWithScores(key21, 1); responseData.push([ "zrandmemberWithCountWithScores(key21, 1)", - [Buffer.from("one"), 1.0], + [["one", 1.0]], ]); if (gte(version, "6.2.0")) { From 1a898cf63067683d765dc5546de5701ed5eac326 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 10:28:25 -0700 Subject: [PATCH 103/236] Node: Add `LCS` command. (#2049) * Add `LCS` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 +- node/src/BaseClient.ts | 113 ++++++++++++++++++++- node/src/Commands.ts | 24 +++++ node/src/Transaction.ts | 69 ++++++++++++- node/tests/RedisClusterClient.test.ts | 11 +-- node/tests/SharedTests.ts | 137 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 59 ++++++++++- 8 files changed, 397 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b30c888b02..bf092e926d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) * Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 08dcbb8021..23fd2c3caa 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -116,7 +116,6 @@ function initialize() { ListDirection, ExpireOptions, FlushMode, - GeoUnit, InfoOptions, InsertPosition, SetOptions, @@ -138,6 +137,7 @@ function initialize() { ConfigurationError, ExecAbortError, RedisError, + ReturnType, RequestError, TimeoutError, ConnectionError, @@ -199,7 +199,6 @@ function initialize() { ListDirection, ExpireOptions, FlushMode, - GeoUnit, InfoOptions, InsertPosition, SetOptions, @@ -221,6 +220,7 @@ function initialize() { ConfigurationError, ExecAbortError, RedisError, + ReturnType, RequestError, TimeoutError, ConnectionError, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0dacbff526..1c69b15643 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -31,7 +31,7 @@ import { GeoUnit, GeospatialData, InsertPosition, - KeyWeight, + KeyWeight, // eslint-disable-line @typescript-eslint/no-unused-vars LPosOptions, ListDirection, MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -84,6 +84,7 @@ import { createIncr, createIncrBy, createIncrByFloat, + createLCS, createLIndex, createLInsert, createLLen, @@ -3940,11 +3941,8 @@ export class BaseClient { * where each sub-array represents a single item in the following order: * * - The member (location) name. - * * - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`. - * * - The geohash of the location as a integer `number`, if `withHash` is set to `true`. - * * - The coordinates as a two item `array` of floating point `number`s, if `withCoord` is set to `true`. * * @example @@ -4173,7 +4171,7 @@ export class BaseClient { * See https://valkey.io/commands/geohash/ for more details. * * @param key - The key of the sorted set. - * @param members - The array of members whose GeoHash strings are to be retrieved. + * @param members - The array of members whose `GeoHash` strings are to be retrieved. * @returns An array of `GeoHash` strings representing the positions of the specified members stored at `key`. * If a member does not exist in the sorted set, a `null` value is returned for that member. * @@ -4191,6 +4189,111 @@ export class BaseClient { ); } + /** + * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * @returns A `String` containing all the longest common subsequence combined between the 2 strings. + * An empty `String` is returned if the keys do not exist or have no common subsequences. + * + * @example + * ```typescript + * await client.mset({"testKey1": "abcd", "testKey2": "axcd"}); + * const result = await client.lcs("testKey1", "testKey2"); + * console.log(result); // Output: 'cd' + * ``` + */ + public async lcs(key1: string, key2: string): Promise { + return this.createWritePromise(createLCS(key1, key2)); + } + + /** + * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * @returns The total length of all the longest common subsequences between the 2 strings. + * + * @example + * ```typescript + * await client.mset({"testKey1": "abcd", "testKey2": "axcd"}); + * const result = await client.lcsLen("testKey1", "testKey2"); + * console.log(result); // Output: 2 + * ``` + */ + public async lcsLen(key1: string, key2: string): Promise { + return this.createWritePromise(createLCS(key1, key2, { len: true })); + } + + /** + * Returns the indices and lengths of the longest common subsequences between strings stored at + * `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match. + * @param minMatchLen - (Optional) The minimum length of matches to include in the result. + * @returns A `Record` containing the indices of the longest common subsequences between the + * 2 strings and the lengths of the longest common subsequences. The resulting map contains two + * keys, "matches" and "len": + * - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings + * stored as an integer. This value doesn't count towards the `minMatchLen` filter. + * - `"matches"` is mapped to a three dimensional array of integers that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by `key1` and `key2`. + * + * @example + * ```typescript + * await client.mset({"key1": "ohmytext", "key2": "mynewtext"}); + * const result = await client.lcsIdx("key1", "key2"); + * console.log(result); // Output: + * { + * "matches" : + * [ + * [ // first substring match is "text" + * [4, 7], // in `key1` it is located between indices 4 and 7 + * [5, 8], // and in `key2` - in between 5 and 8 + * 4 // the match length, returned if `withMatchLen` set to `true` + * ], + * [ // second substring match is "my" + * [2, 3], // in `key1` it is located between indices 2 and 3 + * [0, 1], // and in `key2` - in between 0 and 1 + * 2 // the match length, returned if `withMatchLen` set to `true` + * ] + * ], + * "len" : 6 // total length of the all matches found + * } + * ``` + */ + public async lcsIdx( + key1: string, + key2: string, + options?: { withMatchLen?: boolean; minMatchLen?: number }, + ): Promise> { + return this.createWritePromise( + createLCS(key1, key2, { idx: options ?? {} }), + ); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 1805b2a7f8..44af182559 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2776,3 +2776,27 @@ export function createZRandMember( return createCommand(RequestType.ZRandMember, args); } + +/** @internal */ +export function createLCS( + key1: string, + key2: string, + options?: { + len?: boolean; + idx?: { withMatchLen?: boolean; minMatchLen?: number }; + }, +): command_request.Command { + const args = [key1, key2]; + + if (options) { + if (options.len) args.push("LEN"); + else if (options.idx) { + args.push("IDX"); + if (options.idx.withMatchLen) args.push("WITHMATCHLEN"); + if (options.idx.minMatchLen !== undefined) + args.push("MINMATCHLEN", options.idx.minMatchLen.toString()); + } + } + + return createCommand(RequestType.LCS, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 36b93f3e87..de661be35a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -99,6 +99,7 @@ import { createIncrBy, createIncrByFloat, createInfo, + createLCS, createLIndex, createLInsert, createLLen, @@ -2364,11 +2365,8 @@ export class BaseTransaction> { * where each sub-array represents a single item in the following order: * * - The member (location) name. - * * - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`. - * * - The geohash of the location as a integer `number`. - * * - The coordinates as a two item `array` of floating point `number`s. */ public geosearch( @@ -2496,7 +2494,7 @@ export class BaseTransaction> { * See https://valkey.io/commands/geohash/ for more details. * * @param key - The key of the sorted set. - * @param members - The array of members whose GeoHash strings are to be retrieved. + * @param members - The array of members whose `GeoHash` strings are to be retrieved. * * Command Response - An array of `GeoHash` strings representing the positions of the specified members stored at `key`. * If a member does not exist in the sorted set, a `null` value is returned for that member. @@ -2504,6 +2502,69 @@ export class BaseTransaction> { public geohash(key: string, members: string[]): T { return this.addAndReturn(createGeoHash(key, members)); } + + /** + * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * + * Command Response - A `String` containing all the longest common subsequence combined between the 2 strings. + * An empty `String` is returned if the keys do not exist or have no common subsequences. + */ + public lcs(key1: string, key2: string): T { + return this.addAndReturn(createLCS(key1, key2)); + } + + /** + * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * + * Command Response - The total length of all the longest common subsequences between the 2 strings. + */ + public lcsLen(key1: string, key2: string): T { + return this.addAndReturn(createLCS(key1, key2, { len: true })); + } + + /** + * Returns the indices and lengths of the longest common subsequences between strings stored at + * `key1` and `key2`. + * + * since Valkey version 7.0.0. + * + * See https://valkey.io/commands/lcs/ for more details. + * + * @param key1 - The key that stores the first string. + * @param key2 - The key that stores the second string. + * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match. + * @param minMatchLen - (Optional) The minimum length of matches to include in the result. + * + * Command Response - A `Record` containing the indices of the longest common subsequences between the + * 2 strings and the lengths of the longest common subsequences. The resulting map contains two + * keys, "matches" and "len": + * - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings + * stored as an integer. This value doesn't count towards the `minMatchLen` filter. + * - `"matches"` is mapped to a three dimensional array of integers that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by `key1` and `key2`. + */ + public lcsIdx( + key1: string, + key2: string, + options?: { withMatchLen?: boolean; minMatchLen?: number }, + ): T { + return this.addAndReturn(createLCS(key1, key2, { idx: options ?? {} })); + } } /** diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 00fe077201..02e6f74943 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -338,17 +338,14 @@ describe("GlideClusterClient", () => { client.zintercard(["abc", "zxy", "lkn"]), client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), + client.lcs("abc", "xyz"), + client.lcsLen("abc", "xyz"), + client.lcsIdx("abc", "xyz"), ); } for (const promise of promises) { - try { - await promise; - } catch (e) { - expect((e as Error).message.toLowerCase()).toContain( - "crossslot", - ); - } + await expect(promise).rejects.toThrowError(/crossslot/i); } client.close(); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1f62c7f1c8..2cfc25b4ca 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5991,6 +5991,143 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lcs %p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const key1 = "{lcs}" + uuidv4(); + const key2 = "{lcs}" + uuidv4(); + const key3 = "{lcs}" + uuidv4(); + const key4 = "{lcs}" + uuidv4(); + + // keys does not exist or is empty + expect(await client.lcs(key1, key2)).toEqual(""); + expect(await client.lcsLen(key1, key2)).toEqual(0); + expect(await client.lcsIdx(key1, key2)).toEqual({ + matches: [], + len: 0, + }); + + // LCS with some strings + expect( + await client.mset({ + [key1]: "abcdefghijk", + [key2]: "defjkjuighijk", + [key3]: "123", + }), + ).toEqual("OK"); + expect(await client.lcs(key1, key2)).toEqual("defghijk"); + expect(await client.lcsLen(key1, key2)).toEqual(8); + + // LCS with only IDX + expect(await client.lcsIdx(key1, key2)).toEqual({ + matches: [ + [ + [6, 10], + [8, 12], + ], + [ + [3, 5], + [0, 2], + ], + ], + len: 8, + }); + expect(await client.lcsIdx(key1, key2, {})).toEqual({ + matches: [ + [ + [6, 10], + [8, 12], + ], + [ + [3, 5], + [0, 2], + ], + ], + len: 8, + }); + expect( + await client.lcsIdx(key1, key2, { withMatchLen: false }), + ).toEqual({ + matches: [ + [ + [6, 10], + [8, 12], + ], + [ + [3, 5], + [0, 2], + ], + ], + len: 8, + }); + + // LCS with IDX and WITHMATCHLEN + expect( + await client.lcsIdx(key1, key2, { withMatchLen: true }), + ).toEqual({ + matches: [ + [[6, 10], [8, 12], 5], + [[3, 5], [0, 2], 3], + ], + len: 8, + }); + + // LCS with IDX and MINMATCHLEN + expect( + await client.lcsIdx(key1, key2, { minMatchLen: 4 }), + ).toEqual({ + matches: [ + [ + [6, 10], + [8, 12], + ], + ], + len: 8, + }); + // LCS with IDX and a negative MINMATCHLEN + expect( + await client.lcsIdx(key1, key2, { minMatchLen: -1 }), + ).toEqual({ + matches: [ + [ + [6, 10], + [8, 12], + ], + [ + [3, 5], + [0, 2], + ], + ], + len: 8, + }); + + // LCS with IDX, MINMATCHLEN, and WITHMATCHLEN + expect( + await client.lcsIdx(key1, key2, { + minMatchLen: 4, + withMatchLen: true, + }), + ).toEqual({ matches: [[[6, 10], [8, 12], 5]], len: 8 }); + + // non-string keys are used + expect(await client.sadd(key4, ["_"])).toEqual(1); + await expect(client.lcs(key1, key4)).rejects.toThrow( + RequestError, + ); + await expect(client.lcsLen(key1, key4)).rejects.toThrow( + RequestError, + ); + await expect(client.lcsIdx(key1, key4)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index f7f189fc74..0267e02263 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -447,9 +447,9 @@ export async function transactionTest( baseTransaction: Transaction | ClusterTransaction, version: string, ): Promise<[string, ReturnType][]> { - const key1 = "{key}" + uuidv4(); - const key2 = "{key}" + uuidv4(); - const key3 = "{key}" + uuidv4(); + const key1 = "{key}" + uuidv4(); // string + const key2 = "{key}" + uuidv4(); // string + const key3 = "{key}" + uuidv4(); // string const key4 = "{key}" + uuidv4(); const key5 = "{key}" + uuidv4(); const key6 = "{key}" + uuidv4(); @@ -1046,6 +1046,59 @@ export async function transactionTest( withCode: true, }); responseData.push(["functionList({ libName, true})", []]); + + baseTransaction + .mset({ [key1]: "abcd", [key2]: "bcde", [key3]: "wxyz" }) + .lcs(key1, key2) + .lcs(key1, key3) + .lcsLen(key1, key2) + .lcsLen(key1, key3) + .lcsIdx(key1, key2) + .lcsIdx(key1, key2, { minMatchLen: 1 }) + .lcsIdx(key1, key2, { withMatchLen: true }) + .lcsIdx(key1, key2, { withMatchLen: true, minMatchLen: 1 }) + .del([key1, key2, key3]); + + responseData.push( + ['mset({[key1]: "abcd", [key2]: "bcde", [key3]: "wxyz"})', "OK"], + ["lcs(key1, key2)", "bcd"], + ["lcs(key1, key3)", ""], + ["lcsLen(key1, key2)", 3], + ["lcsLen(key1, key3)", 0], + [ + "lcsIdx(key1, key2)", + { + matches: [ + [ + [1, 3], + [0, 2], + ], + ], + len: 3, + }, + ], + [ + "lcsIdx(key1, key2, {minMatchLen: 1})", + { + matches: [ + [ + [1, 3], + [0, 2], + ], + ], + len: 3, + }, + ], + [ + "lcsIdx(key1, key2, {withMatchLen: true})", + { matches: [[[1, 3], [0, 2], 3]], len: 3 }, + ], + [ + "lcsIdx(key1, key2, {withMatchLen: true, minMatchLen: 1})", + { matches: [[[1, 3], [0, 2], 3]], len: 3 }, + ], + ["del([key1, key2, key3])", 3], + ); } return responseData; From 315a0a75f45fd0a6ec72814f3a2a616235f48586 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 30 Jul 2024 14:25:55 -0700 Subject: [PATCH 104/236] add baseclient for pexpiretime Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 29 +++++++++++++++++++++++++++++ node/src/Transaction.ts | 2 ++ 2 files changed, 31 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1c69b15643..45e9ee7ac8 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -59,6 +59,7 @@ import { createExists, createExpire, createExpireAt, + createExpireTime, createFCall, createFCallReadOnly, createGeoAdd, @@ -107,6 +108,7 @@ import { createObjectRefcount, createPExpire, createPExpireAt, + createPExpireTime, createPTTL, createPersist, createPfAdd, @@ -2418,6 +2420,33 @@ export class BaseClient { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. + * + * See https://valkey.io/commands/expiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * @example + * ```typescript + * const result1 = client.expiretime("myKey"); + * console.log(result1); // Output: -2 - myKey doesn't exist. + * + * const result2 = client.set(myKey, "value"); + * console.log(result2); // Output: -1 - myKey has no associate expiration. + * + * client.expire(myKey, 60); + * const result3 = client.expireTime(myKey); + * console.log(result3); // Output: 718614954 + * ``` + * since - Redis version 7.0.0. + */ + public expiretime(key: string): Promise { + return this.createWritePromise(createExpireTime(key)); + } + /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index de661be35a..341cbc8404 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -67,6 +67,7 @@ import { createExists, createExpire, createExpireAt, + createExpireTime, createFCall, createFCallReadOnly, createFlushAll, @@ -123,6 +124,7 @@ import { createObjectRefcount, createPExpire, createPExpireAt, + createPExpireTime, createPTTL, createPersist, createPfAdd, From 464a18514c7bb061053db4e2bb96325a85a26336 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 10:25:27 -0700 Subject: [PATCH 105/236] current work Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 36 +++++++++++++++++++++++++++++++----- node/src/Commands.ts | 14 ++++++++++++++ node/src/Transaction.ts | 26 ++++++++++++++++++++++++++ node/tests/SharedTests.ts | 13 +++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 45e9ee7ac8..1553968c69 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2421,13 +2421,13 @@ export class BaseClient { } /** - * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. * - * See https://valkey.io/commands/expiretime/ for details. + * See https://valkey.io/commands/expiretime/ for details. * - * @param key - The `key` to determine the expiration value of. - * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * * @example * ```typescript @@ -2505,6 +2505,32 @@ export class BaseClient { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. + * + * See https://valkey.io/commands/pexpiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * @example + * ```typescript + * const result1 = client.pexpiretime("myKey"); + * console.log(result1); // Output: -2 - myKey doesn't exist. + * + * const result2 = client.set(myKey, "value"); + * console.log(result2); // Output: -1 - myKey has no associate expiration. + * + * client.expire(myKey, 60); + * const result3 = client.pexpireTime(myKey); + * console.log(result3); // Output: 718614954 + * ``` + * since - Redis version 7.0.0. + */ + public pexpiretime(key: string): Promise { + return this.createWritePromise(createPExpireTime(key)); + } + /** Returns the remaining time to live of `key` that has a timeout. * See https://valkey.io/commands/ttl/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 44af182559..f957c9ad6a 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1265,6 +1265,13 @@ export function createExpireAt( return createCommand(RequestType.ExpireAt, args); } +/** + * @internal + */ +export function createExpireTime(key: string): command_request.Command { + return createCommand(RequestType.ExpireTime, [key]); +} + /** * @internal */ @@ -1295,6 +1302,13 @@ export function createPExpireAt( return createCommand(RequestType.PExpireAt, args); } +/** + * @internal + */ +export function createPExpireTime(key: string): command_request.Command { + return createCommand(RequestType.PExpireTime, [key]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 341cbc8404..021b8d9198 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1314,6 +1314,19 @@ export class BaseTransaction> { return this.addAndReturn(createExpireAt(key, unixSeconds, option)); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. + * + * See https://valkey.io/commands/expiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + */ + public expireTime(key: string): T { + return this.addAndReturn(createExpireTime(key)); + } + /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. @@ -1358,6 +1371,19 @@ export class BaseTransaction> { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. + * + * See https://valkey.io/commands/pexpiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * + * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + */ + public pexpireTime(key: string): T { + return this.addAndReturn(createExpireTime(key)); + } + /** Returns the remaining time to live of `key` that has a timeout. * See https://valkey.io/commands/ttl/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 2cfc25b4ca..59991a1e6e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2778,6 +2778,19 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `expiretime and pexpiretime test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + return; + } + + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `script test_%p`, async (protocol) => { From 24b6f7025bd674c2f1a624df31970146dfec2067 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:21:37 -0700 Subject: [PATCH 106/236] Node: remove to do comment from BLMOVE command (#2054) remove to do comment Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 2cfc25b4ca..7be3f76743 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1796,9 +1796,6 @@ export function runBaseTests(config: { 0.1, ), ).rejects.toThrow(RequestError); - - // TODO: add test case with 0 timeout (no timeout) should never time out, - // but we wrap the test with timeout to avoid test failing or stuck forever }, protocol); }, config.timeout, From a1c4af44abade9d919375b75fd497d1a25c444f6 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 31 Jul 2024 14:22:10 -0700 Subject: [PATCH 107/236] Node: added PFMERGE command (#2053) * Node: added PFMERGE command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 29 +++++++++++++++++ node/src/Commands.ts | 10 ++++++ node/src/Transaction.ts | 15 +++++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 46 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 7 files changed, 104 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf092e926d..8bb26b2040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) +* Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1c69b15643..d5e038f8b1 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -111,6 +111,7 @@ import { createPersist, createPfAdd, createPfCount, + createPfMerge, createRPop, createRPush, createRPushX, @@ -3698,6 +3699,34 @@ export class BaseClient { return this.createWritePromise(createPfCount(keys)); } + /** + * Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is + * treated as one of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. + * + * See https://valkey.io/commands/pfmerge/ for more details. + * + * @remarks When in Cluster mode, all keys in `sourceKeys` and `destination` must map to the same hash slot. + * @param destination - The key of the destination HyperLogLog where the merged data sets will be stored. + * @param sourceKeys - The keys of the HyperLogLog structures to be merged. + * @returns A simple "OK" response. + * + * @example + * ```typescript + * await client.pfadd("hll1", ["a", "b"]); + * await client.pfadd("hll2", ["b", "c"]); + * const result = await client.pfmerge("new_hll", ["hll1", "hll2"]); + * console.log(result); // Output: OK - The value of "hll1" merged with "hll2" was stored in "new_hll". + * const count = await client.pfcount(["new_hll"]); + * console.log(count); // Output: 3 - The approximated cardinality of "new_hll" is 3. + * ``` + */ + public async pfmerge( + destination: string, + sourceKeys: string[], + ): Promise<"OK"> { + return this.createWritePromise(createPfMerge(destination, sourceKeys)); + } + /** Returns the internal encoding for the Redis object stored at `key`. * * See https://valkey.io/commands/object-encoding for more details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 44af182559..b7a91288a3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2260,6 +2260,16 @@ export function createPfCount(keys: string[]): command_request.Command { return createCommand(RequestType.PfCount, keys); } +/** + * @internal + */ +export function createPfMerge( + destination: string, + sourceKey: string[], +): command_request.Command { + return createCommand(RequestType.PfMerge, [destination, ...sourceKey]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index de661be35a..3c6e742c3b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -127,6 +127,7 @@ import { createPersist, createPfAdd, createPfCount, + createPfMerge, createPing, createRPop, createRPush, @@ -2077,6 +2078,20 @@ export class BaseTransaction> { return this.addAndReturn(createPfCount(keys)); } + /** + * Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is + * treated as one of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. + * + * See https://valkey.io/commands/pfmerge/ for more details. + * + * @param destination - The key of the destination HyperLogLog where the merged data sets will be stored. + * @param sourceKeys - The keys of the HyperLogLog structures to be merged. + * Command Response - A simple "OK" response. + */ + public pfmerge(destination: string, sourceKeys: string[]): T { + return this.addAndReturn(createPfMerge(destination, sourceKeys)); + } + /** Returns the internal encoding for the Redis object stored at `key`. * * See https://valkey.io/commands/object-encoding for more details. diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 02e6f74943..a9e8c97c00 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -312,6 +312,7 @@ describe("GlideClusterClient", () => { client.sunionstore("abc", ["zxy", "lkn"]), client.sunion(["abc", "zxy", "lkn"]), client.pfcount(["abc", "zxy", "lkn"]), + client.pfmerge("abc", ["def", "ghi"]), client.sdiff(["abc", "zxy", "lkn"]), client.sdiffstore("abc", ["zxy", "lkn"]), ]; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 7be3f76743..57a638560e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4440,6 +4440,52 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "pfmerget test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const key3 = `{key}-3-${uuidv4()}`; + const stringKey = `{key}-4-${uuidv4()}`; + const nonExistingKey = `{key}-5-${uuidv4()}`; + + expect(await client.pfadd(key1, ["a", "b", "c"])).toEqual(1); + expect(await client.pfadd(key2, ["b", "c", "d"])).toEqual(1); + + // merge into new HyperLogLog data set + expect(await client.pfmerge(key3, [key1, key2])).toEqual("OK"); + expect(await client.pfcount([key3])).toEqual(4); + + // merge into existing HyperLogLog data set + expect(await client.pfmerge(key1, [key2])).toEqual("OK"); + expect(await client.pfcount([key1])).toEqual(4); + + // non-existing source key + expect( + await client.pfmerge(key2, [key1, nonExistingKey]), + ).toEqual("OK"); + expect(await client.pfcount([key2])).toEqual(4); + + // empty source key list + expect(await client.pfmerge(key1, [])).toEqual("OK"); + expect(await client.pfcount([key1])).toEqual(4); + + // source key exists, but it is not a HyperLogLog + await client.set(stringKey, "foo"); + await expect(client.pfmerge(key3, [stringKey])).rejects.toThrow( + RequestError, + ); + + // destination key exists, but it is not a HyperLogLog + await expect(client.pfmerge(stringKey, [key3])).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + // Set command tests async function setWithExpiryOptions(client: BaseClient) { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 0267e02263..58ce767594 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -880,6 +880,8 @@ export async function transactionTest( baseTransaction.pfadd(key11, ["a", "b", "c"]); responseData.push(['pfadd(key11, ["a", "b", "c"])', 1]); + baseTransaction.pfmerge(key11, []); + responseData.push(["pfmerge(key11, [])", "OK"]); baseTransaction.pfcount([key11]); responseData.push(["pfcount([key11])", 3]); baseTransaction.geoadd( From 1241dfeb43e99eb39ebccfad8d567dd8ed5db11e Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 14:37:19 -0700 Subject: [PATCH 108/236] Node: fix lints (#2062) * Fix linting on Node.js --------- Signed-off-by: Yury-Fridlyand --- .github/workflows/lint-ts.yml | 1 + node/npm/glide/index.ts | 1 + node/tests/AsyncClient.test.ts | 2 +- node/tests/SharedTests.ts | 14 ++++++++------ node/tests/TestUtilities.ts | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint-ts.yml b/.github/workflows/lint-ts.yml index 23d6f348e3..72b51ba16c 100644 --- a/.github/workflows/lint-ts.yml +++ b/.github/workflows/lint-ts.yml @@ -14,6 +14,7 @@ on: - node/** - benchmarks/utilities/* - .github/workflows/lint-ts.yml + workflow_dispatch: concurrency: group: node-lint-${{ github.head_ref || github.ref }} diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 23fd2c3caa..12997f2547 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -9,6 +9,7 @@ import { arch, platform } from "process"; let globalObject = global as unknown; +/* eslint-disable @typescript-eslint/no-require-imports */ function loadNativeBinding() { let nativeBinding = null; switch (platform) { diff --git a/node/tests/AsyncClient.test.ts b/node/tests/AsyncClient.test.ts index ec75809878..762e33fede 100644 --- a/node/tests/AsyncClient.test.ts +++ b/node/tests/AsyncClient.test.ts @@ -7,7 +7,7 @@ import { AsyncClient } from "glide-rs"; import RedisServer from "redis-server"; import { runCommonTests } from "./SharedTests"; import { flushallOnPort } from "./TestUtilities"; -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ const FreePort = require("find-free-port"); const PORT_NUMBER = 4000; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 57a638560e..4173725cde 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5647,10 +5647,12 @@ export function runBaseTests(config: { await client.zmpop([key2, key1], ScoreFilter.MAX, 10), ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); - expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN)) - .toBeNull; - expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN, 1)) - .toBeNull; + expect( + await client.zmpop([nonExistingKey], ScoreFilter.MIN), + ).toBeNull(); + expect( + await client.zmpop([nonExistingKey], ScoreFilter.MIN, 1), + ).toBeNull(); // key exists, but it is not a sorted set expect(await client.set(stringKey, "value")).toEqual("OK"); @@ -5748,7 +5750,7 @@ export function runBaseTests(config: { // ensure that command doesn't time out even if timeout > request timeout (250ms by default) expect( await client.bzmpop([nonExistingKey], ScoreFilter.MAX, 0.5), - ).toBeNull; + ).toBeNull(); expect( await client.bzmpop( [nonExistingKey], @@ -5756,7 +5758,7 @@ export function runBaseTests(config: { 0.55, 1, ), - ).toBeNull; + ).toBeNull(); // key exists, but it is not a sorted set expect(await client.set(stringKey, "value")).toEqual("OK"); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 58ce767594..731ac6740a 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -411,7 +411,7 @@ export function validateTransactionResponse( try { expect(response?.[i]).toEqual(expectedResponse); - } catch (e) { + } catch { const expected = expectedResponse instanceof Map ? JSON.stringify(Array.from(expectedResponse.entries())) From 97d9fad6375af342cd0134d407813c0e7fdcc2bd Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:05:01 -0700 Subject: [PATCH 109/236] Node: add TOUCH command (#2055) * Node: add TOUCH command Signed-off-by: aaron-congo Co-authored-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 24 +++++++++++++++++++++++- node/src/Commands.ts | 7 +++++++ node/src/Transaction.ts | 16 +++++++++++++++- node/tests/RedisClusterClient.test.ts | 2 +- node/tests/SharedTests.ts | 20 ++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bb26b2040..0f4283d85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added BITPOS command ([#1998](https://github.com/valkey-io/valkey-glide/pull/1998)) * Node: Added BITFIELD and BITFIELD_RO commands ([#2026](https://github.com/valkey-io/valkey-glide/pull/2026)) +* Node: Added TOUCH command ([#2055](https://github.com/valkey-io/valkey-glide/pull/2055)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d5e038f8b1..9e6a35e768 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -46,6 +46,7 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createBLMove, createBLPop, createBRPop, createBZMPop, @@ -89,7 +90,6 @@ import { createLInsert, createLLen, createLMove, - createBLMove, createLPop, createLPos, createLPush, @@ -136,6 +136,7 @@ import { createSetBit, createStrlen, createTTL, + createTouch, createType, createUnlink, createXAdd, @@ -4323,6 +4324,27 @@ export class BaseClient { ); } + /** + * Updates the last access time of the specified keys. + * + * See https://valkey.io/commands/touch/ for more details. + * + * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + * @param keys - The keys to update the last access time of. + * @returns The number of keys that were updated. A key is ignored if it doesn't exist. + * + * @example + * ```typescript + * await client.set("key1", "value1"); + * await client.set("key2", "value2"); + * const result = await client.touch(["key1", "key2", "nonExistingKey"]); + * console.log(result); // Output: 2 - The last access time of 2 keys has been updated. + * ``` + */ + public touch(keys: string[]): Promise { + return this.createWritePromise(createTouch(keys)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b7a91288a3..19666bc26c 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2810,3 +2810,10 @@ export function createLCS( return createCommand(RequestType.LCS, args); } + +/** + * @internal + */ +export function createTouch(keys: string[]): command_request.Command { + return createCommand(RequestType.Touch, keys); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3c6e742c3b..e0af1dc05c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -44,6 +44,7 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createBLMove, createBLPop, createBRPop, createBZMPop, @@ -104,7 +105,6 @@ import { createLInsert, createLLen, createLMove, - createBLMove, createLPop, createLPos, createLPush, @@ -155,6 +155,7 @@ import { createStrlen, createTTL, createTime, + createTouch, createType, createUnlink, createXAdd, @@ -2580,6 +2581,19 @@ export class BaseTransaction> { ): T { return this.addAndReturn(createLCS(key1, key2, { idx: options ?? {} })); } + + /** + * Updates the last access time of the specified keys. + * + * See https://valkey.io/commands/touch/ for more details. + * + * @param keys - The keys to update the last access time of. + * + * Command Response - The number of keys that were updated. A key is ignored if it doesn't exist. + */ + public touch(keys: string[]): T { + return this.addAndReturn(createTouch(keys)); + } } /** diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index a9e8c97c00..7d25ddc8fe 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -365,7 +365,7 @@ describe("GlideClusterClient", () => { await client.del(["abc", "zxy", "lkn"]); await client.mget(["abc", "zxy", "lkn"]); await client.mset({ abc: "1", zxy: "2", lkn: "3" }); - // TODO touch + await client.touch(["abc", "zxy", "lkn"]); client.close(); }, ); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 4173725cde..ffbcddcfa8 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5904,6 +5904,26 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `touch test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + + expect( + await client.mset({ [key1]: "value1", [key2]: "value2" }), + ).toEqual("OK"); + expect(await client.touch([key1, key2])).toEqual(2); + expect( + await client.touch([key2, nonExistingKey, key1]), + ).toEqual(2); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zrandmember test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 731ac6740a..d6aeef6d5c 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -805,6 +805,8 @@ export async function transactionTest( responseData.push(["rename(key9, key10)", "OK"]); baseTransaction.exists([key10]); responseData.push(["exists([key10])", 1]); + baseTransaction.touch([key10]); + responseData.push(["touch([key10])", 1]); baseTransaction.renamenx(key10, key9); responseData.push(["renamenx(key10, key9)", true]); baseTransaction.exists([key9, key10]); From b7f33bec45909bea5e6de4f5b93825c75d39205b Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:21:18 -0700 Subject: [PATCH 110/236] add transaction tests Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 4 +-- node/src/Transaction.ts | 1 + node/tests/SharedTests.ts | 51 ++++++++++++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 3 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1553968c69..b63a8c608c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2443,7 +2443,7 @@ export class BaseClient { * ``` * since - Redis version 7.0.0. */ - public expiretime(key: string): Promise { + public async expiretime(key: string): Promise { return this.createWritePromise(createExpireTime(key)); } @@ -2527,7 +2527,7 @@ export class BaseClient { * ``` * since - Redis version 7.0.0. */ - public pexpiretime(key: string): Promise { + public async pexpiretime(key: string): Promise { return this.createWritePromise(createPExpireTime(key)); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 021b8d9198..3ff8c58a2d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1321,6 +1321,7 @@ export class BaseTransaction> { * See https://valkey.io/commands/expiretime/ for details. * * @param key - The `key` to determine the expiration value of. + * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ public expireTime(key: string): T { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 59991a1e6e..c47b43453d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2785,7 +2785,56 @@ export function runBaseTests(config: { if (cluster.checkIfServerVersionLessThan("7.0.0")) { return; } - + + const key1 = uuidv4(); + + expect(await client.set(key1, "foo")).toEqual("OK"); + expect(await client.ttl(key1)).toEqual(-1); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expiretime(key1)).toEqual(-1); + expect(await client.pexpiretime(key1)).toEqual(-1); + } + + expect(await client.expire(key1, 10)).toEqual(true); + expect(await client.ttl(key1)).toBeLessThanOrEqual(10); + + // set command clears the timeout. + expect(await client.set(key1, "bar")).toEqual("OK"); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.pexpire(key1, 10000)).toEqual(true); + } else { + expect( + await client.pexpire( + key1, + 10000, + ExpireOptions.HasNoExpiry, + ), + ).toEqual(true); + } + + expect(await client.ttl(key1)).toBeLessThanOrEqual(10000); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expire(key1, 15000)).toEqual(true); + } else { + expect( + await client.pexpire( + key1, + 15000, + ExpireOptions.HasNoExpiry, + ), + ).toEqual(false); + expect(await client.expiretime(key1)).toBeGreaterThan( + Math.floor(Date.now() / 1000), + ); + expect(await client.pexpiretime(key1)).toBeGreaterThan( + Date.now(), + ); + } + + expect(await client.ttl(key1)).toBeLessThan(15000); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 0267e02263..6e4550857a 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -496,6 +496,9 @@ export async function transactionTest( responseData.push(["echo(value)", value]); baseTransaction.persist(key1); responseData.push(["persist(key1)", false]); + // baseTransaction.expireTime(key1); + // responseData.push(["expiretime(key1), "]) + baseTransaction.set(key2, "baz", { returnOldValue: true }); responseData.push(['set(key2, "baz", { returnOldValue: true })', null]); baseTransaction.customCommand(["MGET", key1, key2]); From a38d7e9c58bd6395dfe4b4f299aaee130b41a5be Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:21:41 -0700 Subject: [PATCH 111/236] update changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf092e926d..ba9ea15e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added EXPIRETIME and PEXPIRETIME commands ([]()) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) From 74ef17797e93246d4012235691c943cc1c0e45de Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:27:57 -0700 Subject: [PATCH 112/236] Node: add RANDOMKEY command (#2057) * Node: add RANDOMKEY command Signed-off-by: aaron-congo Signed-off-by: Yi-Pin Chen Co-authored-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/Commands.ts | 5 ++++ node/src/GlideClient.ts | 18 ++++++++++++++ node/src/GlideClusterClient.ts | 23 ++++++++++++++++++ node/src/Transaction.ts | 12 ++++++++++ node/tests/RedisClient.test.ts | 32 +++++++++++++++++++++++++ node/tests/RedisClusterClient.test.ts | 34 ++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 2 ++ 8 files changed, 126 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f4283d85a..205a3f91c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) * Node: Added GETBIT command ([#1989](https://github.com/valkey-io/valkey-glide/pull/1989)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) +* Node: Added RANDOMKEY command ([#2057](https://github.com/valkey-io/valkey-glide/pull/2057)) * 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)) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 19666bc26c..d8125adc36 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2817,3 +2817,8 @@ export function createLCS( export function createTouch(keys: string[]): command_request.Command { return createCommand(RequestType.Touch, keys); } + +/** @internal */ +export function createRandomKey(): command_request.Command { + return createCommand(RequestType.RandomKey, []); +} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 09af14ed92..d5fa21f885 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -35,6 +35,7 @@ import { createLolwut, createPing, createPublish, + createRandomKey, createSelect, createTime, } from "./Commands"; @@ -609,4 +610,21 @@ export class GlideClient extends BaseClient { public publish(message: string, channel: string): Promise { return this.createWritePromise(createPublish(message, channel)); } + + /** + * Returns a random existing key name from the currently selected database. + * + * See https://valkey.io/commands/randomkey/ for more details. + * + * @returns A random existing key name from the currently selected database. + * + * @example + * ```typescript + * const result = await client.randomKey(); + * console.log(result); // Output: "key12" - "key12" is a random existing key name from the currently selected database. + * ``` + */ + public randomKey(): Promise { + return this.createWritePromise(createRandomKey()); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c6b47226f0..c574fe0ddd 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -37,6 +37,7 @@ import { createLolwut, createPing, createPublish, + createRandomKey, createTime, } from "./Commands"; import { RequestError } from "./Errors"; @@ -983,4 +984,26 @@ export class GlideClusterClient extends BaseClient { createPublish(message, channel, sharded), ); } + + /** + * Returns a random existing key name. + * + * See https://valkey.io/commands/randomkey/ for more details. + * + * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, + * in which case the client will route the command to the nodes defined by `route`. + * @returns A random existing key name. + * + * @example + * ```typescript + * const result = await client.randomKey(); + * console.log(result); // Output: "key12" - "key12" is a random existing key name. + * ``` + */ + public randomKey(route?: Routes): Promise { + return this.createWritePromise( + createRandomKey(), + toProtobufRoute(route), + ); + } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e0af1dc05c..1ab380a103 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -132,6 +132,7 @@ import { createRPop, createRPush, createRPushX, + createRandomKey, createRename, createRenameNX, createSAdd, @@ -2594,6 +2595,17 @@ export class BaseTransaction> { public touch(keys: string[]): T { return this.addAndReturn(createTouch(keys)); } + + /** + * Returns a random existing key name from the currently selected database. + * + * See https://valkey.io/commands/randomkey/ for more details. + * + * Command Response - A random existing key name from the currently selected database. + */ + public randomKey(): T { + return this.addAndReturn(createRandomKey()); + } } /** diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 566eff9adc..6c9986e6e5 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -668,6 +668,38 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "randomKey test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + + // setup: delete all keys in DB 0 and DB 1 + expect(await client.select(0)).toEqual("OK"); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.select(1)).toEqual("OK"); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + + // no keys exist so randomKey returns null + expect(await client.randomKey()).toBeNull(); + // set `key` in DB 1 + expect(await client.set(key, "foo")).toEqual("OK"); + // `key` should be the only key in the database + expect(await client.randomKey()).toEqual(key); + + // switch back to DB 0 + expect(await client.select(0)).toEqual("OK"); + // DB 0 should still have no keys, so randomKey should still return null + expect(await client.randomKey()).toBeNull(); + + 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 7d25ddc8fe..91af181a74 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -2,7 +2,14 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, +} from "@jest/globals"; import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; import { @@ -982,4 +989,29 @@ describe("GlideClusterClient", () => { ); }, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `randomKey test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + + // setup: delete all keys + expect(await client.flushall(FlushMode.SYNC)).toEqual("OK"); + + // no keys exist so randomKey returns null + expect(await client.randomKey()).toBeNull(); + + expect(await client.set(key, "foo")).toEqual("OK"); + // `key` should be the only existing key, so randomKey should return `key` + expect(await client.randomKey()).toEqual(key); + expect(await client.randomKey("allPrimaries")).toEqual(key); + + client.close(); + }, + TIMEOUT, + ); }); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d6aeef6d5c..af9c812825 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -484,6 +484,8 @@ export async function transactionTest( responseData.push(["dbsize()", 0]); baseTransaction.set(key1, "bar"); responseData.push(['set(key1, "bar")', "OK"]); + baseTransaction.randomKey(); + responseData.push(["randomKey()", key1]); baseTransaction.getdel(key1); responseData.push(["getdel(key1)", "bar"]); baseTransaction.set(key1, "bar"); From d0366a3b2b7b2e0a393b0a9bf4a60e6396fa0aae Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:49:45 -0700 Subject: [PATCH 113/236] implement expiretime series Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 32 ++++++++++++++++---------------- node/tests/TestUtilities.ts | 10 ++++++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c47b43453d..301d3659a4 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2786,55 +2786,55 @@ export function runBaseTests(config: { return; } - const key1 = uuidv4(); + const key = uuidv4(); - expect(await client.set(key1, "foo")).toEqual("OK"); - expect(await client.ttl(key1)).toEqual(-1); + expect(await client.set(key, "foo")).toEqual("OK"); + expect(await client.ttl(key)).toEqual(-1); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expiretime(key1)).toEqual(-1); - expect(await client.pexpiretime(key1)).toEqual(-1); + expect(await client.expiretime(key)).toEqual(-1); + expect(await client.pexpiretime(key)).toEqual(-1); } - expect(await client.expire(key1, 10)).toEqual(true); - expect(await client.ttl(key1)).toBeLessThanOrEqual(10); + expect(await client.expire(key, 10)).toEqual(true); + expect(await client.ttl(key)).toBeLessThanOrEqual(10); // set command clears the timeout. - expect(await client.set(key1, "bar")).toEqual("OK"); + expect(await client.set(key, "bar")).toEqual("OK"); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.pexpire(key1, 10000)).toEqual(true); + expect(await client.pexpire(key, 10000)).toEqual(true); } else { expect( await client.pexpire( - key1, + key, 10000, ExpireOptions.HasNoExpiry, ), ).toEqual(true); } - expect(await client.ttl(key1)).toBeLessThanOrEqual(10000); + expect(await client.ttl(key)).toBeLessThanOrEqual(10000); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expire(key1, 15000)).toEqual(true); + expect(await client.expire(key, 15000)).toEqual(true); } else { expect( await client.pexpire( - key1, + key, 15000, ExpireOptions.HasNoExpiry, ), ).toEqual(false); - expect(await client.expiretime(key1)).toBeGreaterThan( + expect(await client.expiretime(key)).toBeGreaterThan( Math.floor(Date.now() / 1000), ); - expect(await client.pexpiretime(key1)).toBeGreaterThan( + expect(await client.pexpiretime(key)).toBeGreaterThan( Date.now(), ); } - expect(await client.ttl(key1)).toBeLessThan(15000); + expect(await client.ttl(key)).toBeLessThan(15000); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6e4550857a..7d95623aa4 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -496,8 +496,14 @@ export async function transactionTest( responseData.push(["echo(value)", value]); baseTransaction.persist(key1); responseData.push(["persist(key1)", false]); - // baseTransaction.expireTime(key1); - // responseData.push(["expiretime(key1), "]) + + if (gte(version, "7.0.0")) { + baseTransaction.expireTime(key1); + responseData.push(["expiretime(key1)", -1]); + + baseTransaction.pexpireTime(key1); + responseData.push(["pexpiretime(key1)", -1]); + } baseTransaction.set(key2, "baz", { returnOldValue: true }); responseData.push(['set(key2, "baz", { returnOldValue: true })', null]); From f168b71f178efb4430a52b2300bf4b7b7bbe418f Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:51:42 -0700 Subject: [PATCH 114/236] 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 ba9ea15e55..d3ba6dd211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* Node: Added EXPIRETIME and PEXPIRETIME commands ([]()) +* Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) From 29d30e39bc81f856f8423250627cce414964e22d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:57:09 -0700 Subject: [PATCH 115/236] update transaction Signed-off-by: Chloe Yip --- node/src/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index eebcd24331..09ebc0a2e9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1385,7 +1385,7 @@ export class BaseTransaction> { * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ public pexpireTime(key: string): T { - return this.addAndReturn(createExpireTime(key)); + return this.addAndReturn(createPExpireTime(key)); } /** Returns the remaining time to live of `key` that has a timeout. From d785b9531cedc0096dfa7348607136a7ef85234c Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 31 Jul 2024 16:39:00 -0700 Subject: [PATCH 116/236] Node: Add `ZLEXCOUNT` command (#2022) * Node: Add ZLEXCOUNT Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 6 +- node/src/BaseClient.ts | 41 +++++++++- node/src/Commands.ts | 46 +++++++++-- node/src/Transaction.ts | 22 ++++++ node/tests/SharedTests.ts | 147 ++++++++++++++++++++++++++++-------- node/tests/TestUtilities.ts | 25 +++++- 7 files changed, 241 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205a3f91c5..427e34e3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) +* Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 12997f2547..b64b44aa86 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -121,7 +121,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - ScoreBoundry, + InfScoreBoundary, + ScoreBoundary, UpdateOptions, ProtocolVersion, RangeByIndex, @@ -204,7 +205,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - ScoreBoundry, + InfScoreBoundary, + ScoreBoundary, UpdateOptions, ProtocolVersion, RangeByIndex, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9e6a35e768..d87696ec90 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -152,6 +152,7 @@ import { createZIncrBy, createZInterCard, createZInterstore, + createZLexCount, createZMPop, createZMScore, createZPopMax, @@ -2855,7 +2856,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of the zcount method to count members in a sorted set within a score range - * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, "positiveInfinity"); + * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that there are 2 members with scores between 5.0 (inclusive) and +inf in the sorted set "my_sorted_set". * ``` * @@ -2899,7 +2900,7 @@ export class BaseClient { * ```typescript * // Example usage of zrange method to retrieve members within a score range in ascending order * const result = await client.zrange("my_sorted_set", { - * start: "negativeInfinity", + * start: InfScoreBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -2941,7 +2942,7 @@ export class BaseClient { * ```typescript * // Example usage of zrangeWithScores method to retrieve members within a score range with their scores * const result = await client.zrangeWithScores("my_sorted_set", { - * start: "negativeInfinity", + * start: InfScoreBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -3271,7 +3272,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of zremRangeByScore method to remove members from a sorted set based on score range - * const result = await client.zremRangeByScore("my_sorted_set", { bound: 5.0, isInclusive: true }, "positiveInfinity"); + * const result = await client.zremRangeByScore("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that 2 members with scores between 5.0 (inclusive) and +inf have been removed from the sorted set "my_sorted_set". * ``` * @@ -3292,6 +3293,38 @@ export class BaseClient { ); } + /** + * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. + * + * See https://valkey.io/commands/zlexcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * @returns The number of members in the specified lex range. + * If 'key' does not exist, it is treated as an empty sorted set, and the command returns '0'. + * If maxLex is less than minLex, '0' is returned. + * + * @example + * ```typescript + * const result = await client.zlexcount("my_sorted_set", {value: "c"}, InfScoreBoundary.PositiveInfinity); + * console.log(result); // Output: 2 - Indicates that there are 2 members with lex scores between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set". + * ``` + * + * @example + * ```typescript + * const result = await client.zlexcount("my_sorted_set", {value: "c"}, {value: "k", isInclusive: false}); + * console.log(result); // Output: 1 - Indicates that there is one member with a lex score between "c" (inclusive) and "k" (exclusive) in the sorted set "my_sorted_set". + * ``` + */ + public async zlexcount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): Promise { + return this.createWritePromise(createZLexCount(key, minLex, maxLex)); + } + /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d8125adc36..0ce84f71b3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1503,15 +1503,25 @@ export function createZMScore( return createCommand(RequestType.ZMScore, [key, ...members]); } -export type ScoreBoundary = +export enum InfScoreBoundary { /** * Positive infinity bound for sorted set. */ - | `positiveInfinity` + PositiveInfinity = "+", /** * Negative infinity bound for sorted set. */ - | `negativeInfinity` + NegativeInfinity = "-", +} + +/** + * Defines where to insert new elements into a list. + */ +export type ScoreBoundary = + /** + * Represents an lower/upper boundary in a sorted set. + */ + | InfScoreBoundary /** * Represents a specific numeric score boundary in a sorted set. */ @@ -1591,10 +1601,16 @@ function getScoreBoundaryArg( score: ScoreBoundary | ScoreBoundary, isLex: boolean = false, ): string { - if (score == "positiveInfinity") { - return isLex ? "+" : "+inf"; - } else if (score == "negativeInfinity") { - return isLex ? "-" : "-inf"; + if (score == InfScoreBoundary.PositiveInfinity) { + return ( + InfScoreBoundary.PositiveInfinity.toString() + (isLex ? "" : "inf") + ); + } + + if (score == InfScoreBoundary.NegativeInfinity) { + return ( + InfScoreBoundary.NegativeInfinity.toString() + (isLex ? "" : "inf") + ); } if (score.isInclusive == false) { @@ -1800,6 +1816,22 @@ export function createPersist(key: string): command_request.Command { return createCommand(RequestType.Persist, [key]); } +/** + * @internal + */ +export function createZLexCount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, +): command_request.Command { + const args = [ + key, + getScoreBoundaryArg(minLex, true), + getScoreBoundaryArg(maxLex, true), + ]; + return createCommand(RequestType.ZLexCount, args); +} + export function createZRank( key: string, member: string, diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 1ab380a103..038f549a1c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -172,6 +172,7 @@ import { createZIncrBy, createZInterCard, createZInterstore, + createZLexCount, createZMPop, createZMScore, createZPopMax, @@ -1793,6 +1794,27 @@ export class BaseTransaction> { ); } + /** + * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. + * + * See https://valkey.io/commands/zlexcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * + * Command Response - The number of members in the specified lex range. + * If 'key' does not exist, it is treated as an empty sorted set, and the command returns '0'. + * If maxLex is less than minLex, '0' is returned. + */ + public zlexcount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): T { + return this.addAndReturn(createZLexCount(key, minLex, maxLex)); + } + /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ffbcddcfa8..d2e45fb13d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -27,6 +27,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + InfScoreBoundary, InfoOptions, InsertPosition, ListDirection, @@ -3261,8 +3262,8 @@ export function runBaseTests(config: { expect( await client.zcount( key1, - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(3); expect( @@ -3280,26 +3281,38 @@ export function runBaseTests(config: { ), ).toEqual(2); expect( - await client.zcount(key1, "negativeInfinity", { - value: 3, - }), + await client.zcount( + key1, + InfScoreBoundary.NegativeInfinity, + { + value: 3, + }, + ), ).toEqual(3); expect( - await client.zcount(key1, "positiveInfinity", { - value: 3, - }), + await client.zcount( + key1, + InfScoreBoundary.PositiveInfinity, + { + value: 3, + }, + ), ).toEqual(0); expect( await client.zcount( "nonExistingKey", - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(0); expect(await client.set(key2, "foo")).toEqual("OK"); await expect( - client.zcount(key2, "negativeInfinity", "positiveInfinity"), + client.zcount( + key2, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), ).rejects.toThrow(); }, protocol); }, @@ -3353,14 +3366,14 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), ).toEqual(["one", "two"]); const result = await client.zrangeWithScores(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, type: "byScore", }); @@ -3376,7 +3389,7 @@ export function runBaseTests(config: { key, { start: { value: 3, isInclusive: false }, - stop: "negativeInfinity", + stop: InfScoreBoundary.NegativeInfinity, type: "byScore", }, true, @@ -3385,8 +3398,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byScore", }), @@ -3396,7 +3409,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3406,7 +3419,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3416,7 +3429,7 @@ export function runBaseTests(config: { await client.zrangeWithScores( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3426,7 +3439,7 @@ export function runBaseTests(config: { expect( await client.zrangeWithScores(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3446,7 +3459,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -3454,8 +3467,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byLex", }), @@ -3466,7 +3479,7 @@ export function runBaseTests(config: { key, { start: { value: "c", isInclusive: false }, - stop: "negativeInfinity", + stop: InfScoreBoundary.NegativeInfinity, type: "byLex", }, true, @@ -3477,7 +3490,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }, @@ -3487,7 +3500,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -4240,15 +4253,15 @@ export function runBaseTests(config: { await client.zremRangeByScore( key, { value: 1 }, - "negativeInfinity", + InfScoreBoundary.NegativeInfinity, ), ).toEqual(0); expect( await client.zremRangeByScore( "nonExistingKey", - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(0); }, protocol); @@ -4256,6 +4269,80 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zlexcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const stringKey = uuidv4(); + const membersScores = { a: 1, b: 2, c: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + // In range negative to positive infinity. + expect( + await client.zlexcount( + key, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(3); + + // In range a (exclusive) to positive infinity + expect( + await client.zlexcount( + key, + { value: "a", isInclusive: false }, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(2); + + // In range negative infinity to c (inclusive) + expect( + await client.zlexcount( + key, + InfScoreBoundary.NegativeInfinity, + { + value: "c", + isInclusive: true, + }, + ), + ).toEqual(3); + + // Incorrect range start > end + expect( + await client.zlexcount( + key, + InfScoreBoundary.PositiveInfinity, + { + value: "c", + isInclusive: true, + }, + ), + ).toEqual(0); + + // Non-existing key + expect( + await client.zlexcount( + "non_existing_key", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(0); + + // Key exists, but it is not a set + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.zlexcount( + stringKey, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "time test_%p", async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index af9c812825..610da75184 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -23,6 +23,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + InfScoreBoundary, InsertPosition, ListDirection, ProtocolVersion, @@ -727,8 +728,24 @@ export async function transactionTest( responseData.push(["zinterstore(key12, [key12, key13])", 2]); } - baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); - responseData.push(['zcount(key8, { value: 2 }, "positiveInfinity")', 4]); + baseTransaction.zcount( + key8, + { value: 2 }, + InfScoreBoundary.PositiveInfinity, + ); + responseData.push([ + "zcount(key8, { value: 2 }, InfScoreBoundary.PositiveInfinity)", + 4, + ]); + baseTransaction.zlexcount( + key8, + { value: "a" }, + InfScoreBoundary.PositiveInfinity, + ); + responseData.push([ + 'zlexcount(key8, { value: "a" }, InfScoreBoundary.PositiveInfinity)', + 4, + ]); baseTransaction.zpopmin(key8); responseData.push(["zpopmin(key8)", { member2: 3.0 }]); baseTransaction.zpopmax(key8); @@ -737,8 +754,8 @@ export async function transactionTest( responseData.push(["zremRangeByRank(key8, 1, 1)", 1]); baseTransaction.zremRangeByScore( key8, - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ); responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty From 4632300e454a31ce6361150d31db6006401102a7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 17:07:03 -0700 Subject: [PATCH 117/236] Node: Add `LASTSAVE` command. (#2059) * Add `LASTSAVE` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Guian Gumpac --- CHANGELOG.md | 1 + node/src/Commands.ts | 5 +++++ node/src/GlideClient.ts | 18 ++++++++++++++++++ node/src/GlideClusterClient.ts | 27 +++++++++++++++++++++++++-- node/src/Transaction.ts | 13 +++++++++++++ node/tests/RedisClient.test.ts | 2 +- node/tests/SharedTests.ts | 30 ++++++++++++++++++++++++++++++ 7 files changed, 93 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427e34e3a8..8718417c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0ce84f71b3..e89d188eec 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2819,6 +2819,11 @@ export function createZRandMember( return createCommand(RequestType.ZRandMember, args); } +/** @internal */ +export function createLastSave(): command_request.Command { + return createCommand(RequestType.LastSave, []); +} + /** @internal */ export function createLCS( key1: string, diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index d5fa21f885..aaeb277b60 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -32,6 +32,7 @@ import { createFunctionList, createFunctionLoad, createInfo, + createLastSave, createLolwut, createPing, createPublish, @@ -611,6 +612,23 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createPublish(message, channel)); } + /** + * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * See https://valkey.io/commands/lastsave/ for more details. + * + * @returns `UNIX TIME` of the last DB save executed with success. + * @example + * ```typescript + * const timestamp = await client.lastsave(); + * console.log("Last DB save was done at " + timestamp); + * ``` + */ + public async lastsave(): Promise { + return this.createWritePromise(createLastSave()); + } + /** * Returns a random existing key name from the currently selected database. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c574fe0ddd..d01459d970 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -34,6 +34,7 @@ import { createFunctionList, createFunctionLoad, createInfo, + createLastSave, createLolwut, createPing, createPublish, @@ -935,7 +936,7 @@ export class GlideClusterClient extends BaseClient { * * 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 + * @param route - The command will be routed to all primary nodes, 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. @@ -946,7 +947,7 @@ export class GlideClusterClient extends BaseClient { * console.log("Number of keys across all primary nodes: ", numKeys); * ``` */ - public dbsize(route?: Routes): Promise> { + public dbsize(route?: Routes): Promise { return this.createWritePromise(createDBSize(), toProtobufRoute(route)); } @@ -985,6 +986,28 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * See https://valkey.io/commands/lastsave/ for more details. + * + * @param route - (Optional) 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 `UNIX TIME` of the last DB save executed with success. + * @example + * ```typescript + * const timestamp = await client.lastsave(); + * console.log("Last DB save was done at " + timestamp); + * ``` + */ + public async lastsave(route?: Routes): Promise> { + return this.createWritePromise( + createLastSave(), + toProtobufRoute(route), + ); + } + /** * Returns a random existing key name. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 038f549a1c..280d8efec7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -187,6 +187,7 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + createLastSave, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2542,6 +2543,18 @@ export class BaseTransaction> { return this.addAndReturn(createGeoHash(key, members)); } + /** + * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * See https://valkey.io/commands/lastsave/ for more details. + * + * Command Response - `UNIX TIME` of the last DB save executed with success. + */ + public lastsave(): T { + return this.addAndReturn(createLastSave()); + } + /** * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 6c9986e6e5..f6d7435e83 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -12,7 +12,7 @@ import { } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; -import { GlideClient, ProtocolVersion, Transaction, ListDirection } from ".."; +import { GlideClient, ListDirection, ProtocolVersion, Transaction } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d2e45fb13d..5859659ffb 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -20,6 +20,7 @@ import { BitmapIndexType, BitwiseOperation, ClosingError, + ClusterTransaction, ConditionalChange, ExpireOptions, FlushMode, @@ -37,6 +38,7 @@ import { Script, SignedEncoding, SortOrder, + Transaction, UnsignedEncoding, UpdateByScore, parseInfoResponse, @@ -304,6 +306,34 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "lastsave %p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const today = new Date(); + today.setDate(today.getDate() - 1); + const yesterday = today.getTime() / 1000; // as epoch time + + expect(await client.lastsave()).toBeGreaterThan(yesterday); + + if (client instanceof GlideClusterClient) { + Object.values(await client.lastsave("allNodes")).forEach( + (v) => expect(v).toBeGreaterThan(yesterday), + ); + } + + const response = + client instanceof GlideClient + ? await client.exec(new Transaction().lastsave()) + : await client.exec( + new ClusterTransaction().lastsave(), + ); + expect(response?.[0]).toBeGreaterThan(yesterday); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `testing mset and mget with multiple existing keys and one non existing key_%p`, async (protocol) => { From efb6577e925db61142e9222916c04c627c727f2a Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 31 Jul 2024 17:12:11 -0700 Subject: [PATCH 118/236] Node: Add zremrangebylex command (#2025) * Node: Add zermrangebylex command Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 45 ++++++++++++++++++++++++++--- node/src/Commands.ts | 16 +++++++++++ node/src/Transaction.ts | 22 +++++++++++++++ node/tests/SharedTests.ts | 56 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 6 ++++ 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8718417c3e..975866273a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) +* Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d87696ec90..0bac9ea262 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -162,6 +162,7 @@ import { createZRangeWithScores, createZRank, createZRem, + createZRemRangeByLex, createZRemRangeByRank, createZRemRangeByScore, createZRevRank, @@ -2856,14 +2857,14 @@ export class BaseClient { * @example * ```typescript * // Example usage of the zcount method to count members in a sorted set within a score range - * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); + * const result = await client.zcount("my_sorted_set", { value: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that there are 2 members with scores between 5.0 (inclusive) and +inf in the sorted set "my_sorted_set". * ``` * * @example * ```typescript * // Example usage of the zcount method to count members in a sorted set within a score range - * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, { bound: 10.0, isInclusive: false }); + * const result = await client.zcount("my_sorted_set", { value: 5.0, isInclusive: true }, { value: 10.0, isInclusive: false }); * console.log(result); // Output: 1 - Indicates that there is one member with score between 5.0 (inclusive) and 10.0 (exclusive) in the sorted set "my_sorted_set". * ``` */ @@ -3259,6 +3260,42 @@ export class BaseClient { return this.createWritePromise(createZRemRangeByRank(key, start, end)); } + /** + * Removes all elements in the sorted set stored at `key` with lexicographical order between `minLex` and `maxLex`. + * + * See https://valkey.io/commands/zremrangebylex/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * @returns The number of members removed. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + * If `minLex` is greater than `maxLex`, 0 is returned. + * + * @example + * ```typescript + * // Example usage of zremRangeByLex method to remove members from a sorted set based on lexicographical order range + * const result = await client.zremRangeByLex("my_sorted_set", { value: "a", isInclusive: false }, { value: "e" }); + * console.log(result); // Output: 4 - Indicates that 4 members, with lexicographical values ranging from "a" (exclusive) to "e" (inclusive), have been removed from "my_sorted_set". + * ``` + * + * @example + * ```typescript + * // Example usage of zremRangeByLex method when the sorted set does not exist + * const result = await client.zremRangeByLex("non_existing_sorted_set", InfScoreBoundary.NegativeInfinity, { value: "e" }); + * console.log(result); // Output: 0 - Indicates that no elements were removed. + * ``` + */ + public zremRangeByLex( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): Promise { + return this.createWritePromise( + createZRemRangeByLex(key, minLex, maxLex), + ); + } + /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. * See https://valkey.io/commands/zremrangebyscore/ for more details. * @@ -3272,14 +3309,14 @@ export class BaseClient { * @example * ```typescript * // Example usage of zremRangeByScore method to remove members from a sorted set based on score range - * const result = await client.zremRangeByScore("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); + * const result = await client.zremRangeByScore("my_sorted_set", { value: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that 2 members with scores between 5.0 (inclusive) and +inf have been removed from the sorted set "my_sorted_set". * ``` * * @example * ```typescript * // Example usage of zremRangeByScore method when the sorted set does not exist - * const result = await client.zremRangeByScore("non_existing_sorted_set", { bound: 5.0, isInclusive: true }, { bound: 10.0, isInclusive: false }); + * const result = await client.zremRangeByScore("non_existing_sorted_set", { value: 5.0, isInclusive: true }, { value: 10.0, isInclusive: false }); * console.log(result); // Output: 0 - Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. * ``` */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e89d188eec..f23b972459 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1798,6 +1798,22 @@ export function createZRemRangeByRank( ]); } +/** + * @internal + */ +export function createZRemRangeByLex( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, +): command_request.Command { + const args = [ + key, + getScoreBoundaryArg(minLex, true), + getScoreBoundaryArg(maxLex, true), + ]; + return createCommand(RequestType.ZRemRangeByLex, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 280d8efec7..79d6d7362b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -182,6 +182,7 @@ import { createZRangeWithScores, createZRank, createZRem, + createZRemRangeByLex, createZRemRangeByRank, createZRemRangeByScore, createZRevRank, @@ -1774,6 +1775,27 @@ export class BaseTransaction> { return this.addAndReturn(createZRemRangeByRank(key, start, end)); } + /** + * Removes all elements in the sorted set stored at `key` with lexicographical order between `minLex` and `maxLex`. + * + * See https://valkey.io/commands/zremrangebylex/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * + * Command Response - The number of members removed. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + * If `minLex` is greater than `maxLex`, 0 is returned. + */ + public zremRangeByLex( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): T { + return this.addAndReturn(createZRemRangeByLex(key, minLex, maxLex)); + } + /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. * See https://valkey.io/commands/zremrangebyscore/ for more details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5859659ffb..e6876170b9 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4263,6 +4263,62 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zremRangeByLex test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const stringKey = uuidv4(); + const membersScores = { a: 1, b: 2, c: 3, d: 4 }; + expect(await client.zadd(key, membersScores)).toEqual(4); + + expect( + await client.zremRangeByLex( + key, + { value: "a", isInclusive: false }, + { value: "c" }, + ), + ).toEqual(2); + + expect( + await client.zremRangeByLex( + key, + { value: "d" }, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(1); + + // MinLex > MaxLex + expect( + await client.zremRangeByLex( + key, + { value: "a" }, + InfScoreBoundary.NegativeInfinity, + ), + ).toEqual(0); + + expect( + await client.zremRangeByLex( + "nonExistingKey", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(0); + + // Key exists, but it is not a set + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.zremRangeByLex( + stringKey, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zremRangeByScore test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 610da75184..99f53b854d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -758,6 +758,12 @@ export async function transactionTest( InfScoreBoundary.PositiveInfinity, ); responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty + baseTransaction.zremRangeByLex( + key8, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ); + responseData.push(["zremRangeByLex(key8, -Inf, +Inf)", 0]); // key8 is already empty if (gte(version, "7.0.0")) { baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); From a0c8f07994eef12d7f148bdf3aeec71fdb7a21da Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 17:53:22 -0700 Subject: [PATCH 119/236] Node: Add `SORT` command. (#2028) * Add `SORT` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 + node/src/Commands.ts | 124 ++++++++++++++ node/src/GlideClient.ts | 99 +++++++++++ node/src/GlideClusterClient.ts | 93 +++++++++++ node/src/Transaction.ts | 139 ++++++++++++++++ node/tests/RedisClient.test.ts | 228 +++++++++++++++++++++++++- node/tests/RedisClusterClient.test.ts | 89 +++++++++- node/tests/TestUtilities.ts | 37 ++++- 9 files changed, 803 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975866273a..2d9f057baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index b64b44aa86..f881d7c054 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -130,6 +130,8 @@ function initialize() { RangeByLex, ReadFrom, RedisCredentials, + SortClusterOptions, + SortOptions, SortedSetRange, StreamTrimOptions, StreamAddOptions, @@ -214,6 +216,8 @@ function initialize() { RangeByLex, ReadFrom, RedisCredentials, + SortClusterOptions, + SortOptions, SortedSetRange, StreamTrimOptions, StreamAddOptions, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f23b972459..105d8bbca4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2804,6 +2804,130 @@ export function createZIncrBy( ]); } +/** + * Optional arguments to {@link GlideClient.sort|sort}, {@link GlideClient.sortStore|sortStore} and {@link GlideClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ for more details. + */ +export type SortOptions = SortBaseOptions & { + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if `key` + * contains IDs of objects, `byPattern` can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + byPattern?: string; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at `key`. + * The pattern should contain an asterisk (`*`) as a placeholder for the element values, where the + * value from `key` replaces the asterisk to create the `key` name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * `key` contains IDs of users, `getPatterns` can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * `getPatterns` is `name_*`, the command will return the values of the keys + * `name_` for each sorted element. Multiple `getPatterns` + * arguments can be provided to retrieve multiple attributes. The special value `#` can + * be used to include the actual element from `key` being sorted. If not provided, only + * the sorted elements themselves are returned. + */ + getPatterns?: string[]; +}; + +type SortBaseOptions = { + /** + * Limiting the range of the query by setting offset and result count. See {@link Limit} class for + * more information. + */ + limit?: Limit; + + /** Options for sorting order of elements. */ + orderBy?: SortOrder; + + /** + * When `true`, sorts elements lexicographically. When `false` (default), + * sorts elements numerically. Use this when the list, set, or sorted set contains string values + * that cannot be converted into double precision floating point numbers. + */ + isAlpha?: boolean; +}; + +/** + * Optional arguments to {@link GlideClusterClient.sort|sort}, {@link GlideClusterClient.sortStore|sortStore} and {@link GlideClusterClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ for more details. + */ +export type SortClusterOptions = SortBaseOptions; + +/** + * The `LIMIT` argument is commonly used to specify a subset of results from the + * matching elements, similar to the `LIMIT` clause in SQL (e.g., `SELECT LIMIT offset, count`). + */ +export type Limit = { + /** The starting position of the range, zero based. */ + offset: number; + /** The maximum number of elements to include in the range. A negative count returns all elements from the offset. */ + count: number; +}; + +/** @internal */ +export function createSort( + key: string, + options?: SortOptions, + destination?: string, +): command_request.Command { + return createSortImpl(RequestType.Sort, key, options, destination); +} + +/** @internal */ +export function createSortReadOnly( + key: string, + options?: SortOptions, +): command_request.Command { + return createSortImpl(RequestType.SortReadOnly, key, options); +} + +/** @internal */ +function createSortImpl( + cmd: RequestType, + key: string, + options?: SortOptions, + destination?: string, +): command_request.Command { + const args: string[] = [key]; + + if (options) { + if (options.limit) { + args.push( + "LIMIT", + options.limit.offset.toString(), + options.limit.count.toString(), + ); + } + + if (options.orderBy) { + args.push(options.orderBy); + } + + if (options.isAlpha) { + args.push("ALPHA"); + } + + if (options.byPattern) { + args.push("BY", options.byPattern); + } + + if (options.getPatterns) { + options.getPatterns.forEach((p) => args.push("GET", p)); + } + } + + if (destination) args.push("STORE", destination); + + return createCommand(cmd, args); +} + /** * @internal */ diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index aaeb277b60..7bd383c320 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -7,6 +7,7 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { @@ -15,6 +16,7 @@ import { FunctionListResponse, InfoOptions, LolwutOptions, + SortOptions, createClientGetName, createClientId, createConfigGet, @@ -38,6 +40,8 @@ import { createPublish, createRandomKey, createSelect, + createSort, + createSortReadOnly, createTime, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; @@ -612,6 +616,101 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createPublish(message, channel)); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@link SortOptions}. + * @returns An `Array` of sorted elements. + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const result = await client.sort("user_ids", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(result); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age + * ``` + */ + public async sort( + key: string, + options?: SortOptions, + ): Promise<(string | null)[]> { + return this.createWritePromise(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@link SortOptions}. + * @returns An `Array` of sorted elements + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const result = await client.sortReadOnly("user_ids", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(result); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age + * ``` + */ + public async sortReadOnly( + key: string, + options?: SortOptions, + ): Promise<(string | null)[]> { + return this.createWritePromise(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - The {@link SortOptions}. + * @returns The number of elements in the sorted key stored at `destination`. + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const sortedElements = await client.sortStore("user_ids", "sortedList", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(sortedElements); // Output: 2 - number of elements sorted and stored + * console.log(await client.lrange("sortedList", 0, -1)); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age stored in `sortedList` + * ``` + */ + public async sortStore( + key: string, + destination: string, + options?: SortOptions, + ): Promise { + return this.createWritePromise(createSort(key, options, destination)); + } + /** * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d01459d970..a95d3bdfa7 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -7,6 +7,7 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { @@ -15,6 +16,7 @@ import { FunctionListResponse, InfoOptions, LolwutOptions, + SortClusterOptions, createClientGetName, createClientId, createConfigGet, @@ -38,6 +40,8 @@ import { createLolwut, createPing, createPublish, + createSort, + createSortReadOnly, createRandomKey, createTime, } from "./Commands"; @@ -986,6 +990,95 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns An `Array` of sorted elements. + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const result = await client.sort("mylist", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(result); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically + * ``` + */ + public async sort( + key: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns An `Array` of sorted elements + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const result = await client.sortReadOnly("mylist", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(result); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically + * ``` + */ + public async sortReadOnly( + key: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns The number of elements in the sorted key stored at `destination`. + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const sortedElements = await client.sortReadOnly("mylist", "sortedList", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(sortedElements); // Output: 3 - number of elements sorted and stored + * console.log(await client.lrange("sortedList", 0, -1)); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically and stored in `sortedList` + * ``` + */ + public async sortStore( + key: string, + destination: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSort(key, options, destination)); + } + /** * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 79d6d7362b..37cb7b380b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,6 +2,10 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars +} from "./BaseClient"; + import { AggregationType, BitFieldGet, @@ -188,6 +192,10 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + createSort, + SortOptions, + createSortReadOnly, + SortClusterOptions, createLastSave, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2699,6 +2707,70 @@ export class Transaction extends BaseTransaction { return this.addAndReturn(createSelect(index)); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - An `Array` of sorted elements. + */ + public sort(key: string, options?: SortOptions): Transaction { + return this.addAndReturn(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - An `Array` of sorted elements + */ + public sortReadOnly(key: string, options?: SortOptions): Transaction { + return this.addAndReturn(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - The number of elements in the sorted key stored at `destination`. + */ + public sortStore( + key: string, + destination: string, + options?: SortOptions, + ): Transaction { + return this.addAndReturn(createSort(key, options, destination)); + } + /** * Copies the value stored at the `source` to the `destination` key. If `destinationDB` is specified, * the value will be copied to the database specified, otherwise the current database will be used. @@ -2741,6 +2813,73 @@ export class Transaction extends BaseTransaction { export class ClusterTransaction extends BaseTransaction { /// TODO: add all CLUSTER commands + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - An `Array` of sorted elements. + */ + public sort(key: string, options?: SortClusterOptions): ClusterTransaction { + return this.addAndReturn(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - An `Array` of sorted elements + */ + public sortReadOnly( + key: string, + options?: SortClusterOptions, + ): ClusterTransaction { + return this.addAndReturn(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - The number of elements in the sorted key stored at `destination`. + */ + public sortStore( + key: string, + destination: string, + options?: SortClusterOptions, + ): ClusterTransaction { + return this.addAndReturn(createSort(key, options, destination)); + } + /** * Copies the value stored at the `source` to the `destination` key. When `replace` is true, * removes the `destination` key first if it already exists, otherwise performs no action. diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index f6d7435e83..c168284057 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -14,7 +14,7 @@ import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { GlideClient, ListDirection, ProtocolVersion, Transaction } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { @@ -668,6 +668,232 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const setPrefix = "setKey" + uuidv4(); + const hashPrefix = "hashKey" + uuidv4(); + const list = uuidv4(); + const store = uuidv4(); + const names = ["Alice", "Bob", "Charlie", "Dave", "Eve"]; + const ages = ["30", "25", "35", "20", "40"]; + + for (let i = 0; i < ages.length; i++) { + expect( + await client.hset(setPrefix + (i + 1), { + name: names[i], + age: ages[i], + }), + ).toEqual(2); + } + + expect(await client.rpush(list, ["3", "1", "5", "4", "2"])).toEqual( + 5, + ); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name", setPrefix + "*->age"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "40", "Charlie", "35"]); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + expect(await client.sort(list, { byPattern: "noSort" })).toEqual([ + "3", + "1", + "5", + "4", + "2", + ]); + + // Non-existent key in the GET pattern results in nulls + expect( + await client.sort(list, { + isAlpha: true, + getPatterns: ["missing"], + }), + ).toEqual([null, null, null, null, null]); + + // Missing key in the set + expect(await client.lpush(list, ["42"])).toEqual(6); + expect( + await client.sort(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + expect(await client.lpop(list)).toEqual("42"); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + byPattern: setPrefix + "*->age", + getPatterns: [ + setPrefix + "*->name", + setPrefix + "*->age", + ], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "40", "Charlie", "35"]); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + expect( + await client.sortReadOnly(list, { byPattern: "noSort" }), + ).toEqual(["3", "1", "5", "4", "2"]); + + // Non-existent key in the GET pattern results in nulls + expect( + await client.sortReadOnly(list, { + isAlpha: true, + getPatterns: ["missing"], + }), + ).toEqual([null, null, null, null, null]); + + // Missing key in the set + expect(await client.lpush(list, ["42"])).toEqual(6); + expect( + await client.sortReadOnly(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + expect(await client.lpop(list)).toEqual("42"); + } + + // SORT with STORE + expect( + await client.sortStore(list, store, { + limit: { offset: 0, count: -1 }, + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.ASC, + }), + ).toEqual(5); + expect(await client.lrange(store, 0, -1)).toEqual([ + "Dave", + "Bob", + "Alice", + "Charlie", + "Eve", + ]); + expect( + await client.sortStore(list, store, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(5); + expect(await client.lrange(store, 0, -1)).toEqual([ + "Dave", + "Bob", + "Alice", + "Charlie", + "Eve", + ]); + + // transaction test + const transaction = new Transaction() + .hset(hashPrefix + 1, { name: "Alice", age: "30" }) + .hset(hashPrefix + 2, { name: "Bob", age: "25" }) + .del([list]) + .lpush(list, ["2", "1"]) + .sort(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .sort(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }) + .sortStore(list, store, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .lrange(store, 0, -1) + .sortStore(list, store, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }) + .lrange(store, 0, -1); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + transaction + .sortReadOnly(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .sortReadOnly(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }); + } + + const expectedResult = [ + 2, + 2, + 1, + 2, + ["Bob", "Alice"], + ["Alice", "Bob"], + 2, + ["Bob", "Alice"], + 2, + ["Alice", "Bob"], + ]; + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expectedResult.push(["Bob", "Alice"], ["Alice", "Bob"]); + } + + const result = await client.exec(transaction); + expect(result).toEqual(expectedResult); + + client.close(); + }, + TIMEOUT, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "randomKey test_%p", async (protocol) => { diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 91af181a74..7421d7bb3c 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -20,11 +20,12 @@ import { InfoOptions, ListDirection, ProtocolVersion, + RequestError, Routes, ScoreFilter, } from ".."; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -322,6 +323,8 @@ describe("GlideClusterClient", () => { client.pfmerge("abc", ["def", "ghi"]), client.sdiff(["abc", "zxy", "lkn"]), client.sdiffstore("abc", ["zxy", "lkn"]), + client.sortStore("abc", "zyx"), + client.sortStore("abc", "zyx", { isAlpha: true }), ]; if (gte(cluster.getVersion(), "6.2.0")) { @@ -624,6 +627,90 @@ describe("GlideClusterClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const key1 = "{sort}" + uuidv4(); + const key2 = "{sort}" + uuidv4(); + const key3 = "{sort}" + uuidv4(); + const key4 = "{sort}" + uuidv4(); + const key5 = "{sort}" + uuidv4(); + + expect(await client.sort(key3)).toEqual([]); + expect(await client.lpush(key1, ["2", "1", "4", "3"])).toEqual(4); + expect(await client.sort(key1)).toEqual(["1", "2", "3", "4"]); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.sortReadOnly(key3)).toEqual([]); + expect(await client.sortReadOnly(key1)).toEqual([ + "1", + "2", + "3", + "4", + ]); + } + + // sort with store + expect(await client.sortStore(key1, key2)).toEqual(4); + expect(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "2", + "3", + "4", + ]); + + // SORT with strings require ALPHA + expect( + await client.rpush(key3, ["2", "1", "a", "x", "c", "4", "3"]), + ).toEqual(7); + await expect(client.sort(key3)).rejects.toThrow(RequestError); + expect(await client.sort(key3, { isAlpha: true })).toEqual([ + "1", + "2", + "3", + "4", + "a", + "c", + "x", + ]); + + // check transaction and options + const transaction = new ClusterTransaction() + .lpush(key4, ["3", "1", "2"]) + .sort(key4, { + orderBy: SortOrder.DESC, + limit: { count: 2, offset: 0 }, + }) + .sortStore(key4, key5, { + orderBy: SortOrder.ASC, + limit: { count: 100, offset: 1 }, + }) + .lrange(key5, 0, -1); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + transaction.sortReadOnly(key4, { + orderBy: SortOrder.DESC, + limit: { count: 2, offset: 0 }, + }); + } + + const result = await client.exec(transaction); + const expectedResult = [3, ["3", "2"], 2, ["2", "3"]]; + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expectedResult.push(["3", "2"]); + } + + expect(result).toEqual(expectedResult); + + client.close(); + }, + ); + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "Protocol is RESP2 = %s", (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 99f53b854d..e87deb5c21 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -468,7 +468,9 @@ export async function transactionTest( const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap const key20 = "{key}" + uuidv4(); // list - const key21 = "{key}" + uuidv4(); // zset random + const key21 = "{key}" + uuidv4(); // list for sort + const key22 = "{key}" + uuidv4(); // list for sort + const key23 = "{key}" + uuidv4(); // zset random const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -939,15 +941,15 @@ export async function transactionTest( 'geohash(key18, ["Palermo", "Catania", "NonExisting"])', ["sqc8b49rny0", "sqdtr74hyu0", null], ]); - baseTransaction.zadd(key21, { one: 1.0 }); - responseData.push(["zadd(key21, {one: 1.0}", 1]); - baseTransaction.zrandmember(key21); - responseData.push(["zrandmember(key21)", "one"]); - baseTransaction.zrandmemberWithCount(key21, 1); - responseData.push(["zrandmemberWithCount(key21, 1)", ["one"]]); - baseTransaction.zrandmemberWithCountWithScores(key21, 1); + baseTransaction.zadd(key23, { one: 1.0 }); + responseData.push(["zadd(key23, {one: 1.0}", 1]); + baseTransaction.zrandmember(key23); + responseData.push(["zrandmember(key23)", "one"]); + baseTransaction.zrandmemberWithCount(key23, 1); + responseData.push(["zrandmemberWithCount(key23, 1)", ["one"]]); + baseTransaction.zrandmemberWithCountWithScores(key23, 1); responseData.push([ - "zrandmemberWithCountWithScores(key21, 1)", + "zrandmemberWithCountWithScores(key23, 1)", [["one", 1.0]], ]); @@ -1130,5 +1132,22 @@ export async function transactionTest( ); } + baseTransaction + .lpush(key21, ["3", "1", "2"]) + .sort(key21) + .sortStore(key21, key22) + .lrange(key22, 0, -1); + responseData.push( + ['lpush(key21, ["3", "1", "2"])', 3], + ["sort(key21)", ["1", "2", "3"]], + ["sortStore(key21, key22)", 3], + ["lrange(key22, 0, -1)", ["1", "2", "3"]], + ); + + if (gte("7.0.0", version)) { + baseTransaction.sortReadOnly(key21); + responseData.push(["sortReadOnly(key21)", ["1", "2", "3"]]); + } + return responseData; } From 108c7092cd2a60da9df9efadd1de8fb98c473ae3 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:22:09 +0300 Subject: [PATCH 120/236] Node: fix SORT_RO version check on transaction (#2071) Signed-off-by: Shoham Elias --- node/tests/TestUtilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e87deb5c21..1c86078c4e 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1144,7 +1144,7 @@ export async function transactionTest( ["lrange(key22, 0, -1)", ["1", "2", "3"]], ); - if (gte("7.0.0", version)) { + if (gte(version, "7.0.0")) { baseTransaction.sortReadOnly(key21); responseData.push(["sortReadOnly(key21)", ["1", "2", "3"]]); } From d212a19b9cb008239bcc895efce1b6f4ce8d2fc2 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:45:10 +0300 Subject: [PATCH 121/236] Node: add publish command to transaction (#2070) Signed-off-by: Shoham Elias --- node/src/GlideClient.ts | 1 + node/src/GlideClusterClient.ts | 3 +- node/src/Transaction.ts | 46 ++++++++++++++++++++++++--- node/tests/RedisClusterClient.test.ts | 9 +++++- node/tests/TestUtilities.ts | 4 +++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 7bd383c320..582b9cd3b5 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -598,6 +598,7 @@ export class GlideClient extends BaseClient { } /** Publish a message on pubsub channel. + * * See https://valkey.io/commands/publish for more details. * * @param message - Message to publish. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index a95d3bdfa7..464df80f6b 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -40,9 +40,9 @@ import { createLolwut, createPing, createPublish, + createRandomKey, createSort, createSortReadOnly, - createRandomKey, createTime, } from "./Commands"; import { RequestError } from "./Errors"; @@ -959,6 +959,7 @@ export class GlideClusterClient extends BaseClient { * 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. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 37cb7b380b..a8968c754f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -44,6 +44,8 @@ import { ScoreFilter, SearchOrigin, SetOptions, + SortClusterOptions, + SortOptions, StreamAddOptions, StreamReadOptions, StreamTrimOptions, @@ -117,6 +119,7 @@ import { createLRem, createLSet, createLTrim, + createLastSave, createLolwut, createMGet, createMSet, @@ -133,6 +136,7 @@ import { createPfCount, createPfMerge, createPing, + createPublish, createRPop, createRPush, createRPushX, @@ -157,6 +161,8 @@ import { createSelect, createSet, createSetBit, + createSort, + createSortReadOnly, createStrlen, createTTL, createTime, @@ -192,11 +198,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - createSort, - SortOptions, - createSortReadOnly, - SortClusterOptions, - createLastSave, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2797,6 +2798,20 @@ export class Transaction extends BaseTransaction { ): Transaction { return this.addAndReturn(createCopy(source, destination, options)); } + + /** 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. + * + * Command Response - Number of subscriptions in primary node that received the message. + * Note that this value does not include subscriptions that configured on replicas. + */ + public publish(message: string, channel: string): Transaction { + return this.addAndReturn(createPublish(message, channel)); + } } /** @@ -2904,4 +2919,25 @@ export class ClusterTransaction extends BaseTransaction { createCopy(source, destination, { replace: replace }), ); } + + /** 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. + * + * Command Response - Number of subscriptions in primary node that received the message. + */ + public publish( + message: string, + channel: string, + sharded: boolean = false, + ): ClusterTransaction { + return this.addAndReturn(createPublish(message, channel, sharded)); + } } diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 7421d7bb3c..f5c3b1603b 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -24,8 +24,8 @@ import { Routes, ScoreFilter, } from ".."; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -246,10 +246,17 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new ClusterTransaction(); + const expectedRes = await transactionTest( transaction, cluster.getVersion(), ); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + transaction.publish("message", "key"); + expectedRes.push(['publish("message", "key")', 0]); + } + const result = await client.exec(transaction); validateTransactionResponse(result, expectedRes); }, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 1c86078c4e..ad59b47f9c 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -475,6 +475,10 @@ export async function transactionTest( const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value const responseData: [string, ReturnType][] = []; + + baseTransaction.publish("test_message", key1); + responseData.push(['publish("test_message", key1)', 0]); + baseTransaction.flushall(); responseData.push(["flushall()", "OK"]); baseTransaction.flushall(FlushMode.SYNC); From 89629cb57a6226c2b894d65e223f4a95eb28a3b3 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:00:22 +0300 Subject: [PATCH 122/236] Python: fix pubsub tests to skip if v<7.0.0 (#2074) --- python/python/tests/test_pubsub.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/python/tests/test_pubsub.py b/python/python/tests/test_pubsub.py index 79735d4282..fd30c4ff70 100644 --- a/python/python/tests/test_pubsub.py +++ b/python/python/tests/test_pubsub.py @@ -2479,6 +2479,9 @@ async def test_pubsub_shardchannels(self, request, cluster_mode: bool): pattern = "test_*" client = await create_client(request, cluster_mode) + min_version = "7.0.0" + if await check_if_server_version_lt(client, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") assert type(client) == GlideClusterClient # Assert no sharded channels exist yet assert await client.pubsub_shardchannels() == [] @@ -2579,6 +2582,9 @@ async def test_pubsub_shardnumsub(self, request, cluster_mode: bool): # Create a client and check initial subscribers client = await create_client(request, cluster_mode) + min_version = "7.0.0" + if await check_if_server_version_lt(client, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") assert type(client) == GlideClusterClient assert await client.pubsub_shardnumsub([channel1, channel2, channel3]) == { channel1_bytes: 0, From 72ce55a118ce9d0bab84ab5435227072b34d35f0 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:40:42 +0300 Subject: [PATCH 123/236] Node: rename ClusterClientConfiguration to GlideClusterClientConfiguration (#2072) --------- Signed-off-by: Shoham Elias --- examples/node/index.ts | 4 +- node/src/BaseClient.ts | 8 +- node/src/GlideClusterClient.ts | 12 +- node/tests/PubSub.test.ts | 219 +++++++++++++----------- node/tests/RedisClientInternals.test.ts | 6 +- 5 files changed, 134 insertions(+), 115 deletions(-) diff --git a/examples/node/index.ts b/examples/node/index.ts index 372b1ff845..49fa9a531d 100644 --- a/examples/node/index.ts +++ b/examples/node/index.ts @@ -12,7 +12,7 @@ async function sendPingToNode() { port: 6379, }, ]; - // Check `GlideClientConfiguration/ClusterClientConfiguration` for additional options. + // Check `GlideClientConfiguration/GlideClusterClientConfiguration` for additional options. const client = await GlideClient.createClient({ addresses: addresses, // if the server uses TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. @@ -41,7 +41,7 @@ async function sendPingToRandomNodeInCluster() { port: 6380, }, ]; - // Check `GlideClientConfiguration/ClusterClientConfiguration` for additional options. + // Check `GlideClientConfiguration/GlideClusterClientConfiguration` for additional options. const client = await GlideClusterClient.createClient({ addresses: addresses, // if the cluster nodes use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0bac9ea262..38e5f4ec66 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -179,7 +179,7 @@ import { TimeoutError, } from "./Errors"; import { GlideClientConfiguration } from "./GlideClient"; -import { ClusterClientConfiguration } from "./GlideClusterClient"; +import { GlideClusterClientConfiguration } from "./GlideClusterClient"; import { Logger } from "./Logger"; import { command_request, @@ -353,7 +353,7 @@ export class BaseClient { private config: BaseClientConfiguration | undefined; protected configurePubsub( - options: ClusterClientConfiguration | GlideClientConfiguration, + options: GlideClusterClientConfiguration | GlideClientConfiguration, configuration: connection_request.IConnectionRequest, ) { if (options.pubsubSubscriptions) { @@ -633,13 +633,13 @@ export class BaseClient { } isPubsubConfigured( - config: GlideClientConfiguration | ClusterClientConfiguration, + config: GlideClientConfiguration | GlideClusterClientConfiguration, ): boolean { return !!config.pubsubSubscriptions; } getPubsubCallbackAndContext( - config: GlideClientConfiguration | ClusterClientConfiguration, + config: GlideClientConfiguration | GlideClusterClientConfiguration, /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ ): [((msg: PubSubMsg, context: any) => void) | null | undefined, any] { if (config.pubsubSubscriptions) { diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 464df80f6b..8f1c9ad99c 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -77,7 +77,7 @@ export type PeriodicChecks = | PeriodicChecksManualInterval; /* eslint-disable-next-line @typescript-eslint/no-namespace */ -export namespace ClusterClientConfiguration { +export namespace GlideClusterClientConfiguration { /** * Enum representing pubsub subscription modes. * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. @@ -118,7 +118,7 @@ export namespace ClusterClientConfiguration { context?: any; }; } -export type ClusterClientConfiguration = BaseClientConfiguration & { +export type GlideClusterClientConfiguration = BaseClientConfiguration & { /** * Configure the periodic topology checks. * These checks evaluate changes in the cluster's topology, triggering a slot refresh when detected. @@ -131,7 +131,7 @@ export type ClusterClientConfiguration = BaseClientConfiguration & { * PubSub subscriptions to be used for the client. * Will be applied via SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE commands during connection establishment. */ - pubsubSubscriptions?: ClusterClientConfiguration.PubSubSubscriptions; + pubsubSubscriptions?: GlideClusterClientConfiguration.PubSubSubscriptions; }; /** @@ -285,7 +285,7 @@ export class GlideClusterClient extends BaseClient { * @internal */ protected createClientRequest( - options: ClusterClientConfiguration, + options: GlideClusterClientConfiguration, ): connection_request.IConnectionRequest { const configuration = super.createClientRequest(options); configuration.clusterModeEnabled = true; @@ -311,11 +311,11 @@ export class GlideClusterClient extends BaseClient { } public static async createClient( - options: ClusterClientConfiguration, + options: GlideClusterClientConfiguration, ): Promise { return await super.createClientInternal( options, - (socket: net.Socket, options?: ClusterClientConfiguration) => + (socket: net.Socket, options?: GlideClusterClientConfiguration) => new GlideClusterClient(socket, options), ); } diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts index 5545a94a46..e7d17220d1 100644 --- a/node/tests/PubSub.test.ts +++ b/node/tests/PubSub.test.ts @@ -13,11 +13,11 @@ import { import { v4 as uuidv4 } from "uuid"; import { BaseClientConfiguration, - ClusterClientConfiguration, ConfigurationError, GlideClient, GlideClientConfiguration, GlideClusterClient, + GlideClusterClientConfiguration, ProtocolVersion, PubSubMsg, TimeoutError, @@ -71,14 +71,14 @@ describe("PubSub", () => { async function createClients( clusterMode: boolean, - options: ClusterClientConfiguration | GlideClientConfiguration, - options2: ClusterClientConfiguration | GlideClientConfiguration, + options: GlideClusterClientConfiguration | GlideClientConfiguration, + options2: GlideClusterClientConfiguration | GlideClientConfiguration, pubsubSubscriptions: | GlideClientConfiguration.PubSubSubscriptions - | ClusterClientConfiguration.PubSubSubscriptions, + | GlideClusterClientConfiguration.PubSubSubscriptions, pubsubSubscriptions2?: | GlideClientConfiguration.PubSubSubscriptions - | ClusterClientConfiguration.PubSubSubscriptions, + | GlideClusterClientConfiguration.PubSubSubscriptions, ): Promise<[TGlideClient, TGlideClient]> { let client: TGlideClient | undefined; @@ -220,7 +220,10 @@ describe("PubSub", () => { function createPubSubSubscription( clusterMode: boolean, clusterChannelsAndPatterns: Partial< - Record> + Record< + GlideClusterClientConfiguration.PubSubChannelModes, + Set + > >, standaloneChannelsAndPatterns: Partial< Record> @@ -229,7 +232,7 @@ describe("PubSub", () => { context: PubSubMsg[] | null = null, ) { if (clusterMode) { - const mySubscriptions: ClusterClientConfiguration.PubSubSubscriptions = + const mySubscriptions: GlideClusterClientConfiguration.PubSubSubscriptions = { channelsAndPatterns: clusterChannelsAndPatterns, callback: callback, @@ -248,7 +251,7 @@ describe("PubSub", () => { async function clientCleanup( client: TGlideClient, - clusterModeSubs?: ClusterClientConfiguration.PubSubSubscriptions, + clusterModeSubs?: GlideClusterClientConfiguration.PubSubSubscriptions, ) { if (client === null) { return; @@ -262,12 +265,12 @@ describe("PubSub", () => { if ( channelType === - ClusterClientConfiguration.PubSubChannelModes.Exact.toString() + GlideClusterClientConfiguration.PubSubChannelModes.Exact.toString() ) { cmd = "UNSUBSCRIBE"; } else if ( channelType === - ClusterClientConfiguration.PubSubChannelModes.Pattern.toString() + GlideClusterClientConfiguration.PubSubChannelModes.Pattern.toString() ) { cmd = "PUNSUBSCRIBE"; } else if (!cmeCluster.checkIfServerVersionLessThan("7.0.0")) { @@ -318,7 +321,7 @@ describe("PubSub", () => { `pubsub exact happy path test_%p%p`, async (clusterMode, method) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient; @@ -339,8 +342,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set([channel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -400,7 +403,7 @@ describe("PubSub", () => { "pubsub exact happy path coexistence test_%p", async (clusterMode) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -414,8 +417,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set([channel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -486,7 +489,7 @@ describe("PubSub", () => { "pubsub exact happy path many channels test_%p_%p", async (clusterMode, method) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -516,8 +519,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(channelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set(Object.keys(channelsAndMessages)), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -611,7 +614,7 @@ describe("PubSub", () => { "pubsub exact happy path many channels coexistence test_%p", async (clusterMode) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -633,8 +636,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(channelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set(Object.keys(channelsAndMessages)), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -733,7 +736,7 @@ describe("PubSub", () => { if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -755,8 +758,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set([channel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel]), }, {}, callback, @@ -829,7 +832,7 @@ describe("PubSub", () => { if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -843,8 +846,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( true, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set([channel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel]), }, {}, ); @@ -928,7 +931,7 @@ describe("PubSub", () => { if (cmeCluster.checkIfServerVersionLessThan(minVersion)) return; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -959,8 +962,10 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set(Object.keys(channelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set( + Object.keys(channelsAndMessages), + ), }, {}, callback, @@ -1053,7 +1058,7 @@ describe("PubSub", () => { }; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -1072,8 +1077,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Pattern]: @@ -1163,7 +1168,7 @@ describe("PubSub", () => { }; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -1174,8 +1179,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Pattern]: @@ -1272,7 +1277,7 @@ describe("PubSub", () => { } let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -1290,8 +1295,8 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Pattern]: @@ -1402,7 +1407,7 @@ describe("PubSub", () => { }; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -1420,10 +1425,12 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set( + Object.keys(exactChannelsAndMessages), + ), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -1546,11 +1553,11 @@ describe("PubSub", () => { }; let pubSubExact: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubPattern: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClientExact: TGlideClient | null = null; @@ -1572,8 +1579,10 @@ describe("PubSub", () => { pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set( + Object.keys(exactChannelsAndMessages), + ), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -1594,8 +1603,8 @@ describe("PubSub", () => { pubSubPattern = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Pattern]: @@ -1762,7 +1771,7 @@ describe("PubSub", () => { const publishResponse = 1; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let listeningClient: TGlideClient | null = null; @@ -1780,12 +1789,16 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set(Object.keys(shardedChannelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set( + Object.keys(exactChannelsAndMessages), + ), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set( + Object.keys(shardedChannelsAndMessages), + ), }, {}, callback, @@ -1929,15 +1942,15 @@ describe("PubSub", () => { let publishingClient: TGlideClient | null = null; let pubSubExact: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubPattern: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubSharded: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -1958,8 +1971,10 @@ describe("PubSub", () => { pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set( + Object.keys(exactChannelsAndMessages), + ), }, {}, callback, @@ -1981,8 +1996,8 @@ describe("PubSub", () => { pubSubPattern = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([PATTERN]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([PATTERN]), }, {}, callback, @@ -1996,8 +2011,10 @@ describe("PubSub", () => { pubSubSharded = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set(Object.keys(shardedChannelsAndMessages)), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set( + Object.keys(shardedChannelsAndMessages), + ), }, {}, callback, @@ -2192,15 +2209,15 @@ describe("PubSub", () => { let publishingClient: TGlideClient | null = null; let pubSubExact: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubPattern: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubSharded: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -2221,8 +2238,8 @@ describe("PubSub", () => { pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2244,8 +2261,8 @@ describe("PubSub", () => { pubSubPattern = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2259,8 +2276,8 @@ describe("PubSub", () => { pubSubSharded = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2419,11 +2436,11 @@ describe("PubSub", () => { let clientPattern: TGlideClient | null = null; let pubSubExact: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubPattern: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -2445,8 +2462,8 @@ describe("PubSub", () => { pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([CHANNEL_NAME]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: @@ -2460,8 +2477,8 @@ describe("PubSub", () => { pubSubPattern = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([CHANNEL_NAME]), }, { [GlideClientConfiguration.PubSubChannelModes.Pattern]: @@ -2595,15 +2612,15 @@ describe("PubSub", () => { let clientDontCare: TGlideClient | null = null; let pubSubExact: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubPattern: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; let pubSubSharded: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -2628,8 +2645,8 @@ describe("PubSub", () => { pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2640,8 +2657,8 @@ describe("PubSub", () => { pubSubPattern = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Pattern]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2652,8 +2669,8 @@ describe("PubSub", () => { pubSubSharded = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Sharded]: - new Set([CHANNEL_NAME]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([CHANNEL_NAME]), }, {}, callback, @@ -2818,7 +2835,7 @@ describe("PubSub", () => { "test pubsub exact max size message_%p", async (clusterMode) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -2834,7 +2851,7 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes + [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set([channel]), }, { @@ -2923,7 +2940,7 @@ describe("PubSub", () => { if (cmeCluster.checkIfServerVersionLessThan("7.0.0")) return; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -2938,7 +2955,7 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes + [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set([channel]), }, {}, @@ -3023,7 +3040,7 @@ describe("PubSub", () => { "test pubsub exact max size message callback_%p", async (clusterMode) => { let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -3040,7 +3057,7 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes + [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set([channel]), }, { @@ -3116,7 +3133,7 @@ describe("PubSub", () => { async (clusterMode) => { if (cmeCluster.checkIfServerVersionLessThan("7.0.0")) return; let pubSub: - | ClusterClientConfiguration.PubSubSubscriptions + | GlideClusterClientConfiguration.PubSubSubscriptions | GlideClientConfiguration.PubSubSubscriptions | null = null; @@ -3133,7 +3150,7 @@ describe("PubSub", () => { pubSub = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes + [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set([channel]), }, {}, @@ -3203,7 +3220,7 @@ describe("PubSub", () => { const pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: + [GlideClusterClientConfiguration.PubSubChannelModes.Exact]: new Set([channel]), }, { @@ -3240,7 +3257,7 @@ describe("PubSub", () => { const pubSubExact = createPubSubSubscription( clusterMode, { - [ClusterClientConfiguration.PubSubChannelModes.Exact]: + [GlideClusterClientConfiguration.PubSubChannelModes.Exact]: new Set([channel]), }, { diff --git a/node/tests/RedisClientInternals.test.ts b/node/tests/RedisClientInternals.test.ts index 631c936513..ed5fc35d92 100644 --- a/node/tests/RedisClientInternals.test.ts +++ b/node/tests/RedisClientInternals.test.ts @@ -21,11 +21,11 @@ import { Reader } from "protobufjs"; import { BaseClientConfiguration, ClosingError, - ClusterClientConfiguration, ClusterTransaction, GlideClient, GlideClientConfiguration, GlideClusterClient, + GlideClusterClientConfiguration, InfoOptions, Logger, RequestError, @@ -124,7 +124,9 @@ function sendResponse( function getConnectionAndSocket( checkRequest?: (request: connection_request.ConnectionRequest) => boolean, - connectionOptions?: ClusterClientConfiguration | GlideClientConfiguration, + connectionOptions?: + | GlideClusterClientConfiguration + | GlideClientConfiguration, isCluster?: boolean, ): Promise<{ socket: net.Socket; From 730b9ed0be6a421efb8fb748721e89d5c0c52eb9 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:06:45 +0300 Subject: [PATCH 124/236] Node: rename tests files names to Glide* (#2073) Signed-off-by: Shoham Elias --- node/tests/{RedisClient.test.ts => GlideClient.test.ts} | 0 ...{RedisClientInternals.test.ts => GlideClientInternals.test.ts} | 0 .../{RedisClusterClient.test.ts => GlideClusterClient.test.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename node/tests/{RedisClient.test.ts => GlideClient.test.ts} (100%) rename node/tests/{RedisClientInternals.test.ts => GlideClientInternals.test.ts} (100%) rename node/tests/{RedisClusterClient.test.ts => GlideClusterClient.test.ts} (100%) diff --git a/node/tests/RedisClient.test.ts b/node/tests/GlideClient.test.ts similarity index 100% rename from node/tests/RedisClient.test.ts rename to node/tests/GlideClient.test.ts diff --git a/node/tests/RedisClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts similarity index 100% rename from node/tests/RedisClientInternals.test.ts rename to node/tests/GlideClientInternals.test.ts diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts similarity index 100% rename from node/tests/RedisClusterClient.test.ts rename to node/tests/GlideClusterClient.test.ts From 712cc0bb39c71c028fd741371f205a269392c9be Mon Sep 17 00:00:00 2001 From: barshaul Date: Wed, 17 Jul 2024 15:23:59 +0000 Subject: [PATCH 125/236] Remove branch version from the DEVELOPER.md files Signed-off-by: barshaul --- csharp/DEVELOPER.md | 10 ++++------ go/DEVELOPER.md | 3 +-- java/DEVELOPER.md | 3 +-- node/DEVELOPER.md | 3 +-- python/DEVELOPER.md | 3 +-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/csharp/DEVELOPER.md b/csharp/DEVELOPER.md index a5bb030474..f42e0d2022 100644 --- a/csharp/DEVELOPER.md +++ b/csharp/DEVELOPER.md @@ -80,12 +80,10 @@ source "$HOME/.cargo/env" Before starting this step, make sure you've installed all software requirments. 1. Clone the repository - -```bash -VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch -git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git -cd valkey-glide -``` + ```bash + git clone https://github.com/valkey-io/valkey-glide.git + cd valkey-glide + ``` 2. Initialize git submodule diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index ab89b259b3..7794181aa7 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -102,8 +102,7 @@ Before starting this step, make sure you've installed all software requirements. 1. Clone the repository: ```bash - VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + git clone https://github.com/valkey-io/valkey-glide.git cd glide-for-redis ``` 2. Initialize git submodule: diff --git a/java/DEVELOPER.md b/java/DEVELOPER.md index 413a90f953..bd04f278af 100644 --- a/java/DEVELOPER.md +++ b/java/DEVELOPER.md @@ -84,8 +84,7 @@ Before starting this step, make sure you've installed all software dependencies. 1. Clone the repository: ```bash - VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + git clone https://github.com/valkey-io/valkey-glide.git cd valkey-glide/java ``` 2. Initialize git submodule: diff --git a/node/DEVELOPER.md b/node/DEVELOPER.md index dba47cdef7..ddab936d34 100644 --- a/node/DEVELOPER.md +++ b/node/DEVELOPER.md @@ -62,8 +62,7 @@ Before starting this step, make sure you've installed all software requirments. 1. Clone the repository: ```bash - VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + git clone https://github.com/valkey-io/valkey-glide.git cd valkey-glide ``` 2. Initialize git submodule: diff --git a/python/DEVELOPER.md b/python/DEVELOPER.md index ac9a6555c5..41b37a8438 100644 --- a/python/DEVELOPER.md +++ b/python/DEVELOPER.md @@ -86,8 +86,7 @@ Before starting this step, make sure you've installed all software requirments. 1. Clone the repository: ```bash - VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + git clone https://github.com/valkey-io/valkey-glide.git cd glide-for-redis ``` 2. Initialize git submodule: From bb40440f464b0ce90b1bddf777575e6eb2761783 Mon Sep 17 00:00:00 2001 From: barshaul Date: Wed, 17 Jul 2024 15:26:58 +0000 Subject: [PATCH 126/236] Remove glide-for-redis ref Signed-off-by: barshaul --- go/DEVELOPER.md | 2 +- python/DEVELOPER.md | 2 +- utils/get_licenses_from_ort.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 7794181aa7..22ec84c264 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -103,7 +103,7 @@ Before starting this step, make sure you've installed all software requirements. 1. Clone the repository: ```bash git clone https://github.com/valkey-io/valkey-glide.git - cd glide-for-redis + cd valkey-glide ``` 2. Initialize git submodule: ```bash diff --git a/python/DEVELOPER.md b/python/DEVELOPER.md index 41b37a8438..2f71c6e27c 100644 --- a/python/DEVELOPER.md +++ b/python/DEVELOPER.md @@ -87,7 +87,7 @@ Before starting this step, make sure you've installed all software requirments. 1. Clone the repository: ```bash git clone https://github.com/valkey-io/valkey-glide.git - cd glide-for-redis + cd valkey-glide ``` 2. Initialize git submodule: ```bash diff --git a/utils/get_licenses_from_ort.py b/utils/get_licenses_from_ort.py index c0216343fa..3d9853ea53 100644 --- a/utils/get_licenses_from_ort.py +++ b/utils/get_licenses_from_ort.py @@ -44,7 +44,7 @@ def __init__(self, name: str, ort_results_folder: str) -> None: """ Args: name (str): the language name. - ort_results_folder (str): The relative path to the ort results folder from the root of the glide-for-redis directory. + ort_results_folder (str): The relative path to the ort results folder from the root of the valkey-glide directory. """ folder_path = f"{SCRIPT_PATH}/../{ort_results_folder}" self.analyzer_result_file = f"{folder_path}/analyzer-result.json" From d18cb1d4cda9a5da385f45de5b1412eef2c8a555 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Thu, 1 Aug 2024 00:25:18 +0000 Subject: [PATCH 127/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 58 ++++++++++++++++++++++++++- java/THIRD_PARTY_LICENSES_JAVA | 58 ++++++++++++++++++++++++++- node/THIRD_PARTY_LICENSES_NODE | 60 ++++++++++++++++++++++++++-- python/THIRD_PARTY_LICENSES_PYTHON | 60 ++++++++++++++++++++++++++-- 4 files changed, 226 insertions(+), 10 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 04a0d8e1a8..127c59e8ce 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.6.1 +Package: bytes:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -17346,7 +17346,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.19 +Package: ppv-lite86:0.2.18 The following copyrights and licenses were found in the source code of this package: @@ -31421,6 +31421,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -31673,6 +31700,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy-derive:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy-derive:0.7.35 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 2878da8a61..31f04702ac 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.6.1 +Package: bytes:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -18241,7 +18241,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.19 +Package: ppv-lite86:0.2.18 The following copyrights and licenses were found in the source code of this package: @@ -34377,6 +34377,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -34629,6 +34656,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy-derive:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy-derive:0.7.35 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 f6e379af41..68538b3501 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -3555,7 +3555,7 @@ For more information, please refer to ---- -Package: bytes:1.6.1 +Package: bytes:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -18003,7 +18003,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.19 +Package: ppv-lite86:0.2.18 The following copyrights and licenses were found in the source code of this package: @@ -33681,6 +33681,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -33933,6 +33960,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy-derive:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +37957,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.0.0 +Package: @types:node:22.0.2 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 3b89a9bf00..5d2f4dbb90 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.6.1 +Package: bytes:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -18266,7 +18266,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.19 +Package: ppv-lite86:0.2.18 The following copyrights and licenses were found in the source code of this package: @@ -33939,6 +33939,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -34191,6 +34218,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: zerocopy-derive:0.6.6 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -37951,7 +38005,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: protobuf:5.27.2 +Package: protobuf:5.27.3 The following copyrights and licenses were found in the source code of this package: From 54cfb74d0525aff4401cc248e3038c10d4e19067 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Thu, 1 Aug 2024 10:47:10 -0700 Subject: [PATCH 128/236] Node: Add `ZSCAN` command (#2061) * Added ZSCAN command to Node Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 49 ++++++++++++ node/src/Commands.ts | 45 +++++++++++ node/src/Transaction.ts | 22 ++++++ node/tests/SharedTests.ts | 148 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 7 ++ 7 files changed, 274 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9f057baa..99ac3ee206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index f881d7c054..2a0a57eb2b 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -75,6 +75,7 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { + BaseScanOptions, BitEncoding, BitFieldGet, BitFieldIncrBy, @@ -161,6 +162,7 @@ function initialize() { } = nativeBinding; module.exports = { + BaseScanOptions, BitEncoding, BitFieldGet, BitFieldIncrBy, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 38e5f4ec66..4b552ebcfa 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -11,6 +11,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BaseScanOptions, BitFieldGet, BitFieldIncrBy, // eslint-disable-line @typescript-eslint/no-unused-vars BitFieldOverflow, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -168,6 +169,7 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + createZScan, } from "./Commands"; import { ClosingError, @@ -4236,6 +4238,53 @@ export class BaseClient { return this.createWritePromise(createZIncrBy(key, increment, member)); } + /** + * Iterates incrementally over a sorted set. + * + * See https://valkey.io/commands/zscan for more details. + * + * @param key - The key of the sorted set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of + * the search. + * @param options - (Optional) The zscan options. + * @returns An `Array` of the `cursor` and the subset of the sorted set held by `key`. + * The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + * returned on the last iteration of the sorted set. The second element is always an `Array` of the subset + * of the sorted set held in `key`. The `Array` in the second element is always a flattened series of + * `String` pairs, where the value is at even indices and the score is at odd indices. + * + * @example + * ```typescript + * // Assume "key" contains a sorted set with multiple members + * let newCursor = "0"; + * let result = []; + * + * do { + * result = await client.zscan(key1, newCursor, { + * match: "*", + * count: 5, + * }); + * newCursor = result[0]; + * console.log("Cursor: ", newCursor); + * console.log("Members: ", result[1]); + * } while (newCursor !== "0"); + * // The output of the code above is something similar to: + * // Cursor: 123 + * // Members: ['value 163', '163', 'value 114', '114', 'value 25', '25', 'value 82', '82', 'value 64', '64'] + * // Cursor: 47 + * // Members: ['value 39', '39', 'value 127', '127', 'value 43', '43', 'value 139', '139', 'value 211', '211'] + * // Cursor: 0 + * // Members: ['value 55', '55', 'value 24', '24', 'value 90', '90', 'value 113', '113'] + * ``` + */ + public async zscan( + key: string, + cursor: string, + options?: BaseScanOptions, + ): Promise<[string, string[]]> { + return this.createWritePromise(createZScan(key, cursor, options)); + } + /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 105d8bbca4..2459d93311 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2999,3 +2999,48 @@ export function createTouch(keys: string[]): command_request.Command { export function createRandomKey(): command_request.Command { return createCommand(RequestType.RandomKey, []); } + +/** + * This base class represents the common set of optional arguments for the SCAN family of commands. + * Concrete implementations of this class are tied to specific SCAN commands (SCAN, HSCAN, SSCAN, + * and ZSCAN). + */ +export type BaseScanOptions = { + /** + * The match filter is applied to the result of the command and will only include + * strings that match the pattern specified. If the sorted set is large enough for scan commands to return + * only a subset of the sorted set then there could be a case where the result is empty although there are + * items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates + * that it will only fetch and match `10` items from the list. + */ + readonly match?: string; + /** + * `COUNT` is a just a hint for the command for how many elements to fetch from the + * sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to + * represent the results as compact single-allocation packed encoding. + */ + readonly count?: number; +}; + +/** + * @internal + */ +export function createZScan( + key: string, + cursor: string, + options?: BaseScanOptions, +): command_request.Command { + let args: string[] = [key, cursor]; + + if (options) { + if (options.match) { + args = args.concat("MATCH", options.match); + } + + if (options.count !== undefined) { + args = args.concat("COUNT", options.count.toString()); + } + } + + return createCommand(RequestType.ZScan, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index a8968c754f..59fa7d19c5 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -8,6 +8,7 @@ import { import { AggregationType, + BaseScanOptions, BitFieldGet, BitFieldIncrBy, // eslint-disable-line @typescript-eslint/no-unused-vars BitFieldOverflow, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -197,6 +198,7 @@ import { createZRemRangeByScore, createZRevRank, createZRevRankWithScore, + createZScan, createZScore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2537,6 +2539,26 @@ export class BaseTransaction> { return this.addAndReturn(createZIncrBy(key, increment, member)); } + /** + * Iterates incrementally over a sorted set. + * + * See https://valkey.io/commands/zscan for more details. + * + * @param key - The key of the sorted set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of + * the search. + * @param options - (Optional) The zscan options. + * + * Command Response - An `Array` of the `cursor` and the subset of the sorted set held by `key`. + * The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + * returned on the last iteration of the sorted set. The second element is always an `Array` of the subset + * of the sorted set held in `key`. The `Array` in the second element is always a flattened series of + * `String` pairs, where the value is at even indices and the score is at odd indices. + */ + public zscan(key: string, cursor: string, options?: BaseScanOptions): T { + return this.addAndReturn(createZScan(key, cursor, options)); + } + /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index e6876170b9..1660140a51 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5898,6 +5898,154 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zscan test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const initialCursor = "0"; + const defaultCount = 20; + const resultCursorIndex = 0; + const resultCollectionIndex = 1; + + // Setup test data - use a large number of entries to force an iterative cursor. + const numberMap: Record = {}; + const expectedNumberMapArray: string[] = []; + + for (let i = 0; i < 10000; i++) { + expectedNumberMapArray.push(i.toString()); + expectedNumberMapArray.push(i.toString()); + numberMap[i.toString()] = i; + } + + const charMembers = ["a", "b", "c", "d", "e"]; + const charMap: Record = {}; + const expectedCharMapArray: string[] = []; + + for (let i = 0; i < charMembers.length; i++) { + expectedCharMapArray.push(charMembers[i]); + expectedCharMapArray.push(i.toString()); + charMap[charMembers[i]] = i; + } + + // Empty set + let result = await client.zscan(key1, initialCursor); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + + // Negative cursor + result = await client.zscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + + // Result contains the whole set + expect(await client.zadd(key1, charMap)).toEqual( + charMembers.length, + ); + result = await client.zscan(key1, initialCursor); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex].length).toEqual( + expectedCharMapArray.length, + ); + expect(result[resultCollectionIndex]).toEqual( + expectedCharMapArray, + ); + + result = await client.zscan(key1, initialCursor, { + match: "a", + }); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual(["a", "0"]); + + // Result contains a subset of the key + expect(await client.zadd(key1, numberMap)).toEqual( + Object.keys(numberMap).length, + ); + + result = await client.zscan(key1, initialCursor); + let resultCursor = result[resultCursorIndex]; + let resultIterationCollection = result[resultCollectionIndex]; + let fullResultMapArray: string[] = resultIterationCollection; + let nextResult; + let nextResultCursor; + + // 0 is returned for the cursor of the last iteration. + while (resultCursor != "0") { + nextResult = await client.zscan(key1, resultCursor); + nextResultCursor = nextResult[resultCursorIndex]; + expect(nextResultCursor).not.toEqual(resultCursor); + + expect(nextResult[resultCollectionIndex]).not.toEqual( + resultIterationCollection, + ); + fullResultMapArray = fullResultMapArray.concat( + nextResult[resultCollectionIndex], + ); + resultIterationCollection = + nextResult[resultCollectionIndex]; + resultCursor = nextResultCursor; + } + + // Fetching by cursor is randomized. + const expectedCombinedMapArray = + expectedNumberMapArray.concat(expectedCharMapArray); + expect(fullResultMapArray.length).toEqual( + expectedCombinedMapArray.length, + ); + + for (let i = 0; i < fullResultMapArray.length; i += 2) { + expect(fullResultMapArray).toContain( + expectedCombinedMapArray[i], + ); + } + + // Test match pattern + result = await client.zscan(key1, initialCursor, { + match: "*", + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(defaultCount); + + // Test count + result = await client.zscan(key1, initialCursor, { count: 20 }); + expect(result[resultCursorIndex]).not.toEqual("0"); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(20); + + // Test count with match returns a non-empty list + result = await client.zscan(key1, initialCursor, { + match: "1*", + count: 20, + }); + expect(result[resultCursorIndex]).not.toEqual("0"); + expect(result[resultCollectionIndex].length).toBeGreaterThan(0); + + // Exceptions + // Non-set key + expect(await client.set(key2, "test")).toEqual("OK"); + await expect(client.zscan(key2, initialCursor)).rejects.toThrow( + RequestError, + ); + await expect( + client.zscan(key2, initialCursor, { + match: "test", + count: 20, + }), + ).rejects.toThrow(RequestError); + + // Negative count + await expect( + client.zscan(key2, initialCursor, { count: -1 }), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `bzmpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ad59b47f9c..f5dc724edd 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -715,6 +715,13 @@ export async function transactionTest( ]); baseTransaction.zadd(key12, { one: 1, two: 2 }); responseData.push(["zadd(key12, { one: 1, two: 2 })", 2]); + baseTransaction.zscan(key12, "0"); + responseData.push(['zscan(key12, "0")', ["0", ["one", "1", "two", "2"]]]); + baseTransaction.zscan(key12, "0", { match: "*", count: 20 }); + responseData.push([ + 'zscan(key12, "0", {match: "*", count: 20})', + ["0", ["one", "1", "two", "2"]], + ]); baseTransaction.zadd(key13, { one: 1, two: 2, three: 3.5 }); responseData.push(["zadd(key13, { one: 1, two: 2, three: 3.5 })", 3]); From 6f621dada77e859da845c1d60a4020e9cd26dd5e Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 1 Aug 2024 12:55:30 -0700 Subject: [PATCH 129/236] address comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 24 ++++++++++++++---------- node/src/Transaction.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 537e482f07..fc6f72aa7e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2424,26 +2424,28 @@ export class BaseClient { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * To get the expiration with millisecond precision, use {@link pexpiretime}. * * See https://valkey.io/commands/expiretime/ for details. * * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * + * since - Redis version 7.0.0. + * * @example * ```typescript - * const result1 = client.expiretime("myKey"); + * const result1 = await client.expiretime("myKey"); * console.log(result1); // Output: -2 - myKey doesn't exist. * - * const result2 = client.set(myKey, "value"); - * console.log(result2); // Output: -1 - myKey has no associate expiration. + * const result2 = await client.set(myKey, "value"); + * const result3 = await client.expireTime(myKey); + * console.log(result2); // Output: -1 - myKey has no associated expiration. * * client.expire(myKey, 60); - * const result3 = client.expireTime(myKey); - * console.log(result3); // Output: 718614954 + * const result3 = await client.expireTime(myKey); + * console.log(result3); // Output: 123456 - the Unix timestamp (in seconds) when "myKey" will expire. * ``` - * since - Redis version 7.0.0. */ public async expiretime(key: string): Promise { return this.createWritePromise(createExpireTime(key)); @@ -2515,19 +2517,21 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * + * since - Redis version 7.0.0. + * * @example * ```typescript * const result1 = client.pexpiretime("myKey"); * console.log(result1); // Output: -2 - myKey doesn't exist. * * const result2 = client.set(myKey, "value"); - * console.log(result2); // Output: -1 - myKey has no associate expiration. + * const result3 = client.pexpireTime(myKey); + * console.log(result2); // Output: -1 - myKey has no associated expiration. * * client.expire(myKey, 60); * const result3 = client.pexpireTime(myKey); - * console.log(result3); // Output: 718614954 + * console.log(result3); // Output: 123456789 - the Unix timestamp (in milliseconds) when "myKey" will expire. * ``` - * since - Redis version 7.0.0. */ public async pexpiretime(key: string): Promise { return this.createWritePromise(createPExpireTime(key)); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 09ebc0a2e9..05d3452ab7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1319,7 +1319,7 @@ export class BaseTransaction> { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * To get the expiration with millisecond precision, use {@link pexpiretime}. * * See https://valkey.io/commands/expiretime/ for details. * From 40be5dec1d04d4d5bdc3a9b58d21b3424acbc5b2 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:37:02 -0700 Subject: [PATCH 130/236] Node: Add command SetRange (#2066) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 29 +++++++++++++++++++++++++++++ node/src/Commands.ts | 9 +++++++++ node/src/Transaction.ts | 18 ++++++++++++++++++ node/tests/SharedTests.ts | 30 ++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ac3ee206..9bf594b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) +* Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4b552ebcfa..2cd5025517 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -135,6 +135,7 @@ import { createSUnionStore, createSet, createSetBit, + createSetRange, createStrlen, createTTL, createTouch, @@ -4464,6 +4465,34 @@ export class BaseClient { return this.createWritePromise(createTouch(keys)); } + /** + * Overwrites part of the string stored at `key`, starting at the specified `offset`, + * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, + * the string is padded with zero bytes to make `offset` fit. Creates the `key` if it doesn't exist. + * + * See https://valkey.io/commands/setrange/ for more details. + * + * @param key - The key of the string to update. + * @param offset - The position in the string where `value` should be written. + * @param value - The string written with `offset`. + * @returns The length of the string stored at `key` after it was modified. + * + * @example + * ```typescript + * const len = await client.setrange("key", 6, "GLIDE"); + * console.log(len); // Output: 11 - New key was created with length of 11 symbols + * const value = await client.get("key"); + * console.log(result); // Output: "\0\0\0\0\0\0GLIDE" - The string was padded with zero bytes + * ``` + */ + public async setrange( + key: string, + offset: number, + value: string, + ): Promise { + return this.createWritePromise(createSetRange(key, offset, value)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 2459d93311..c59e56b806 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3044,3 +3044,12 @@ export function createZScan( return createCommand(RequestType.ZScan, args); } + +/** @internal */ +export function createSetRange( + key: string, + offset: number, + value: string, +): command_request.Command { + return createCommand(RequestType.SetRange, [key, offset.toString(), value]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 59fa7d19c5..3292f20d73 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -162,6 +162,7 @@ import { createSelect, createSet, createSetBit, + createSetRange, createSort, createSortReadOnly, createStrlen, @@ -2694,6 +2695,23 @@ export class BaseTransaction> { public randomKey(): T { return this.addAndReturn(createRandomKey()); } + + /** + * Overwrites part of the string stored at `key`, starting at the specified `offset`, + * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, + * the string is padded with zero bytes to make `offset` fit. Creates the `key` if it doesn't exist. + * + * See https://valkey.io/commands/setrange/ for more details. + * + * @param key - The key of the string to update. + * @param offset - The position in the string where `value` should be written. + * @param value - The string written with `offset`. + * + * Command Response - The length of the string stored at `key` after it was modified. + */ + public setrange(key: string, offset: number, value: string): T { + return this.addAndReturn(createSetRange(key, offset, value)); + } } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1660140a51..84def5e10e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4659,6 +4659,36 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "setrange test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonStringKey = uuidv4(); + + // new key + expect(await client.setrange(key, 0, "Hello World")).toBe(11); + + // existing key + expect(await client.setrange(key, 6, "GLIDE")).toBe(11); + expect(await client.get(key)).toEqual("Hello GLIDE"); + + // offset > len + expect(await client.setrange(key, 15, "GLIDE")).toBe(20); + expect(await client.get(key)).toEqual( + "Hello GLIDE\0\0\0\0GLIDE", + ); + + // non-string key + expect(await client.lpush(nonStringKey, ["_"])).toBe(1); + await expect( + client.setrange(nonStringKey, 0, "_"), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + // Set command tests async function setWithExpiryOptions(client: BaseClient) { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index f5dc724edd..686241f2b5 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -517,6 +517,8 @@ export async function transactionTest( responseData.push(["mget([key1, key2])", ["bar", "baz"]]); baseTransaction.strlen(key1); responseData.push(["strlen(key1)", 3]); + baseTransaction.setrange(key1, 0, "GLIDE"); + responseData.push(["setrange(key1, 0, 'GLIDE'", 5]); baseTransaction.del([key1]); responseData.push(["del([key1])", 1]); baseTransaction.hset(key4, { [field]: value }); From 5ae81cc086c3c5682283da97a7cdc20caf5d8e6e Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Thu, 1 Aug 2024 14:16:22 -0700 Subject: [PATCH 131/236] Node: Added `XDEL` command (#2064) * Added XDEL command Signed-off-by: Guian Gumpac * Fixed flaky test Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 21 ++++++++++ node/src/Commands.ts | 13 +++++++ node/src/Transaction.ts | 28 ++++++++++++-- node/tests/SharedTests.ts | 76 ++++++++++++++++++++++++++++++++----- node/tests/TestUtilities.ts | 2 + 6 files changed, 128 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf594b0e6..7e1cb142d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ * Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) +* Node: Added XDEL command ([#2064]((https://github.com/valkey-io/valkey-glide/pull/2064)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 2cd5025517..a7f3608ad6 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -142,6 +142,7 @@ import { createType, createUnlink, createXAdd, + createXDel, createXLen, createXRead, createXTrim, @@ -3489,6 +3490,26 @@ export class BaseClient { return this.createWritePromise(createXAdd(key, values, options)); } + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * See https://valkey.io/commands/xdel for more details. + * + * @param key - The key of the stream. + * @param ids - An array of entry ids. + * @returns The number of entries removed from the stream. This number may be less than the number of entries in + * `ids`, if the specified `ids` don't exist in the stream. + * + * @example + * ```typescript + * console.log(await client.xdel("key", ["1538561698944-0", "1538561698944-1"])); + * // Output is 2 since the stream marked 2 entries as deleted. + * ``` + */ + public xdel(key: string, ids: string[]): Promise { + return this.createWritePromise(createXDel(key, ids)); + } + /** * Trims the stream stored at `key` by evicting older entries. * See https://valkey.io/commands/xtrim/ for more details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index c59e56b806..c7390e85ef 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1933,6 +1933,9 @@ function addTrimOptions(options: StreamTrimOptions, args: string[]) { } } +/** + * @internal + */ export function createXAdd( key: string, values: [string, string][], @@ -1962,6 +1965,16 @@ export function createXAdd( return createCommand(RequestType.XAdd, args); } +/** + * @internal + */ +export function createXDel( + key: string, + ids: string[], +): command_request.Command { + return createCommand(RequestType.XDel, [key, ...ids]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3292f20d73..9f5eab8f0c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -172,6 +172,7 @@ import { createType, createUnlink, createXAdd, + createXDel, createXLen, createXRead, createXTrim, @@ -1984,7 +1985,9 @@ export class BaseTransaction> { * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. - * @returns The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. + * @param options - (Optional) Stream add options. + * + * Command Response - The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. */ public xadd( key: string, @@ -1994,13 +1997,29 @@ export class BaseTransaction> { return this.addAndReturn(createXAdd(key, values, options)); } + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * See https://valkey.io/commands/xdel for more details. + * + * @param key - The key of the stream. + * @param ids - An array of entry ids. + * + * Command Response - The number of entries removed from the stream. This number may be less than the number of entries in + * `ids`, if the specified `ids` don't exist in the stream. + */ + public xdel(key: string, ids: string[]): T { + return this.addAndReturn(createXDel(key, ids)); + } + /** * Trims the stream stored at `key` by evicting older entries. * See https://valkey.io/commands/xtrim/ for more details. * * @param key - the key of the stream * @param options - options detailing how to trim the stream. - * @returns The number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. + * + * Command Response - The number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. */ public xtrim(key: string, options: StreamTrimOptions): T { return this.addAndReturn(createXTrim(key, options)); @@ -2009,7 +2028,7 @@ export class BaseTransaction> { /** Returns the server time. * See https://valkey.io/commands/time/ for details. * - * @returns - The current server time as a two items `array`: + * Command Response - The current server time as a two items `array`: * A Unix timestamp and the amount of microseconds already elapsed in the current second. * The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. */ @@ -2023,7 +2042,8 @@ export class BaseTransaction> { * * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. * @param options - options detailing how to read the stream. - * @returns A map between a stream key, and an array of entries in the matching key. The entries are in an [id, fields[]] format. + * + * Command Response - A map between a stream key, and an array of entries in the matching key. The entries are in an [id, fields[]] format. */ public xread( keys_and_ids: Record, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 84def5e10e..0f5f9f7c10 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5941,11 +5941,8 @@ export function runBaseTests(config: { // Setup test data - use a large number of entries to force an iterative cursor. const numberMap: Record = {}; - const expectedNumberMapArray: string[] = []; - for (let i = 0; i < 10000; i++) { - expectedNumberMapArray.push(i.toString()); - expectedNumberMapArray.push(i.toString()); + for (let i = 0; i < 50000; i++) { numberMap[i.toString()] = i; } @@ -6018,15 +6015,18 @@ export function runBaseTests(config: { } // Fetching by cursor is randomized. - const expectedCombinedMapArray = - expectedNumberMapArray.concat(expectedCharMapArray); + const expectedFullMap: Record = { + ...numberMap, + ...charMap, + }; + expect(fullResultMapArray.length).toEqual( - expectedCombinedMapArray.length, + Object.keys(expectedFullMap).length * 2, ); for (let i = 0; i < fullResultMapArray.length; i += 2) { - expect(fullResultMapArray).toContain( - expectedCombinedMapArray[i], + expect(fullResultMapArray[i] in expectedFullMap).toEqual( + true, ); } @@ -6544,6 +6544,64 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xdel test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const stringKey = uuidv4(); + const nonExistentKey = uuidv4(); + const streamId1 = "0-1"; + const streamId2 = "0-2"; + const streamId3 = "0-3"; + + expect( + await client.xadd( + key, + [ + ["f1", "foo1"], + ["f2", "foo2"], + ], + { id: streamId1 }, + ), + ).toEqual(streamId1); + + expect( + await client.xadd( + key, + [ + ["f1", "foo1"], + ["f2", "foo2"], + ], + { id: streamId2 }, + ), + ).toEqual(streamId2); + + expect(await client.xlen(key)).toEqual(2); + + // deletes one stream id, and ignores anything invalid + expect(await client.xdel(key, [streamId1, streamId3])).toEqual( + 1, + ); + expect(await client.xdel(nonExistentKey, [streamId3])).toEqual( + 0, + ); + + // invalid argument - id list should not be empty + await expect(client.xdel(key, [])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xdel(stringKey, [streamId3]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 686241f2b5..4aa4cabbf9 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -841,6 +841,8 @@ export async function transactionTest( 'xtrim(key9, { method: "minid", threshold: "0-2", exact: true }', 1, ]); + baseTransaction.xdel(key9, ["0-3", "0-5"]); + responseData.push(["xdel(key9, [['0-3', '0-5']])", 1]); baseTransaction.rename(key9, key10); responseData.push(["rename(key9, key10)", "OK"]); baseTransaction.exists([key10]); From ca0b317f5e0f57200215db3de04462fef29c4c22 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:46:47 -0700 Subject: [PATCH 132/236] Node: Add command LMPOP & BLMPOP (#2050) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 66 +++++++++++++++ node/src/Commands.ts | 42 ++++++++++ node/src/Transaction.ts | 49 ++++++++++- node/tests/GlideClient.test.ts | 23 ++++-- node/tests/GlideClusterClient.test.ts | 2 + node/tests/SharedTests.ts | 112 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 19 +++++ 8 files changed, 307 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1cb142d9..41d81b0401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) * Node: Added XDEL command ([#2064]((https://github.com/valkey-io/valkey-glide/pull/2064)) +* Node: Added LMPOP & BLMPOP command ([#2050](https://github.com/valkey-io/valkey-glide/pull/2050)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a7f3608ad6..c04d118091 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -55,6 +55,7 @@ import { createBitField, createBitOp, createBitPos, + createBLMPop, createDecr, createDecrBy, createDel, @@ -91,6 +92,7 @@ import { createLInsert, createLLen, createLMove, + createLMPop, createLPop, createLPos, createLPush, @@ -4514,6 +4516,70 @@ export class BaseClient { return this.createWritePromise(createSetRange(key, offset, value)); } + /** + * Pops one or more elements from the first non-empty list from the provided `keys`. + * + * See https://valkey.io/commands/lmpop/ for more details. + * + * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @param keys - An array of keys to lists. + * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. + * @param count - (Optional) The maximum number of popped elements. + * @returns A `Record` of key-name mapped array of popped elements. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.lpush("testKey", ["one", "two", "three"]); + * await client.lpush("testKey2", ["five", "six", "seven"]); + * const result = await client.lmpop(["testKey", "testKey2"], ListDirection.LEFT, 1L); + * console.log(result.get("testKey")); // Output: { "testKey": ["three"] } + * ``` + */ + public async lmpop( + keys: string[], + direction: ListDirection, + count?: number, + ): Promise> { + return this.createWritePromise(createLMPop(keys, direction, count)); + } + + /** + * Blocks the connection until it pops one or more elements from the first non-empty list from the + * provided `key`. `BLMPOP` is the blocking variant of {@link lmpop}. + * + * See https://valkey.io/commands/blmpop/ for more details. + * + * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @param keys - An array of keys to lists. + * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + * @param count - (Optional) The maximum number of popped elements. + * @returns - A `Record` of `key` name mapped array of popped elements. + * If no member could be popped and the timeout expired, returns `null`. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.lpush("testKey", ["one", "two", "three"]); + * await client.lpush("testKey2", ["five", "six", "seven"]); + * const result = await client.blmpop(["testKey", "testKey2"], ListDirection.LEFT, 0.1, 1L); + * console.log(result.get("testKey")); // Output: { "testKey": ["three"] } + * ``` + */ + public async blmpop( + keys: string[], + direction: ListDirection, + timeout: number, + count?: number, + ): Promise> { + return this.createWritePromise( + createBLMPop(timeout, keys, direction, count), + ); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index c7390e85ef..5416883d9d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3066,3 +3066,45 @@ export function createSetRange( ): command_request.Command { return createCommand(RequestType.SetRange, [key, offset.toString(), value]); } + +/** + * @internal + */ +export function createLMPop( + keys: string[], + direction: ListDirection, + count?: number, +): command_request.Command { + const args: string[] = [keys.length.toString(), ...keys, direction]; + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.LMPop, args); +} + +/** + * @internal + */ +export function createBLMPop( + timeout: number, + keys: string[], + direction: ListDirection, + count?: number, +): command_request.Command { + const args: string[] = [ + timeout.toString(), + keys.length.toString(), + ...keys, + direction, + ]; + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.BLMPop, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 9f5eab8f0c..61f03233cf 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -53,6 +53,7 @@ import { ZAddOptions, createBLMove, createBLPop, + createBLMPop, createBRPop, createBZMPop, createBitCount, @@ -112,6 +113,7 @@ import { createLInsert, createLLen, createLMove, + createLMPop, createLPop, createLPos, createLPush, @@ -2524,8 +2526,7 @@ export class BaseTransaction> { * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. - * @param timeout - The number of seconds to wait for a blocking operation to complete. - * A value of 0 will block indefinitely. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. * * Command Response - A two-element `array` containing the key name of the set from which the element @@ -2732,6 +2733,50 @@ export class BaseTransaction> { public setrange(key: string, offset: number, value: string): T { return this.addAndReturn(createSetRange(key, offset, value)); } + + /** + * Pops one or more elements from the first non-empty list from the provided `keys`. + * + * See https://valkey.io/commands/lmpop/ for more details. + * + * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. + * @param keys - An array of keys to lists. + * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. + * @param count - (Optional) The maximum number of popped elements. + * + * Command Response - A `Record` of `key` name mapped array of popped elements. + * + * since Valkey version 7.0.0. + */ + public lmpop(keys: string[], direction: ListDirection, count?: number): T { + return this.addAndReturn(createLMPop(keys, direction, count)); + } + + /** + * Blocks the connection until it pops one or more elements from the first non-empty list from the + * provided `key`. `BLMPOP` is the blocking variant of {@link lmpop}. + * + * See https://valkey.io/commands/blmpop/ for more details. + * + * @param keys - An array of keys to lists. + * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of + * `0` will block indefinitely. + * @param count - (Optional) The maximum number of popped elements. + * + * Command Response - A `Record` of `key` name mapped array of popped elements. + * If no member could be popped and the timeout expired, returns `null`. + * + * since Valkey version 7.0.0. + */ + public blmpop( + keys: string[], + direction: ListDirection, + timeout: number, + count?: number, + ): T { + return this.addAndReturn(createBLMPop(timeout, keys, direction, count)); + } } /** diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index c168284057..8893c81cf2 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -143,14 +143,27 @@ describe("GlideClient", () => { ListDirection.LEFT, 0.1, ); - const timeoutPromise = new Promise((resolve) => { - setTimeout(resolve, 500); - }); + + const blmpopPromise = client.blmpop( + ["key1", "key2"], + ListDirection.LEFT, + 0.1, + ); + + const promiseList = [blmovePromise, blmpopPromise]; try { - await Promise.race([blmovePromise, timeoutPromise]); + for (const promise of promiseList) { + const timeoutPromise = new Promise((resolve) => { + setTimeout(resolve, 500); + }); + await Promise.race([promise, timeoutPromise]); + } } finally { - Promise.resolve(blmovePromise); + for (const promise of promiseList) { + await Promise.resolve([promise]); + } + client.close(); } }, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index f5c3b1603b..19d7b91c7b 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -332,6 +332,8 @@ describe("GlideClusterClient", () => { client.sdiffstore("abc", ["zxy", "lkn"]), client.sortStore("abc", "zyx"), client.sortStore("abc", "zyx", { isAlpha: true }), + client.lmpop(["abc", "def"], ListDirection.LEFT, 1), + client.blmpop(["abc", "def"], ListDirection.RIGHT, 0.1, 1), ]; if (gte(cluster.getVersion(), "6.2.0")) { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0f5f9f7c10..87c96f32d8 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6602,6 +6602,118 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lmpop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + return; + } + + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const nonListKey = uuidv4(); + const singleKeyArray = [key1]; + const multiKeyArray = [key2, key1]; + const count = 1; + const lpushArgs = ["one", "two", "three", "four", "five"]; + const expected = { [key1]: ["five"] }; + const expected2 = { [key2]: ["one", "two"] }; + + // nothing to be popped + expect( + await client.lmpop( + singleKeyArray, + ListDirection.LEFT, + count, + ), + ).toBeNull(); + + // pushing to the arrays to be popped + expect(await client.lpush(key1, lpushArgs)).toEqual(5); + expect(await client.lpush(key2, lpushArgs)).toEqual(5); + + // checking correct result from popping + expect( + await client.lmpop(singleKeyArray, ListDirection.LEFT), + ).toEqual(expected); + + // popping multiple elements from the right + expect( + await client.lmpop(multiKeyArray, ListDirection.RIGHT, 2), + ).toEqual(expected2); + + // Key exists, but is not a set + expect(await client.set(nonListKey, "lmpop")).toBe("OK"); + await expect( + client.lmpop([nonListKey], ListDirection.RIGHT), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `blmpop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + return; + } + + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const nonListKey = uuidv4(); + const singleKeyArray = [key1]; + const multiKeyArray = [key2, key1]; + const count = 1; + const lpushArgs = ["one", "two", "three", "four", "five"]; + const expected = { [key1]: ["five"] }; + const expected2 = { [key2]: ["one", "two"] }; + + // nothing to be popped + expect( + await client.blmpop( + singleKeyArray, + ListDirection.LEFT, + 0.1, + count, + ), + ).toBeNull(); + + // pushing to the arrays to be popped + expect(await client.lpush(key1, lpushArgs)).toEqual(5); + expect(await client.lpush(key2, lpushArgs)).toEqual(5); + + // checking correct result from popping + expect( + await client.blmpop( + singleKeyArray, + ListDirection.LEFT, + 0.1, + ), + ).toEqual(expected); + + // popping multiple elements from the right + expect( + await client.blmpop( + multiKeyArray, + ListDirection.RIGHT, + 0.1, + 2, + ), + ).toEqual(expected2); + + // Key exists, but is not a set + expect(await client.set(nonListKey, "blmpop")).toBe("OK"); + await expect( + client.blmpop([nonListKey], ListDirection.RIGHT, 0.1, 1), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4aa4cabbf9..34d79f3f05 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -471,6 +471,7 @@ export async function transactionTest( const key21 = "{key}" + uuidv4(); // list for sort const key22 = "{key}" + uuidv4(); // list for sort const key23 = "{key}" + uuidv4(); // zset random + const key24 = "{key}" + uuidv4(); // list value const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -548,6 +549,24 @@ export async function transactionTest( field + "4", ]); responseData.push(["lpush(key5, [1, 2, 3, 4])", 4]); + + if (gte("7.0.0", version)) { + baseTransaction.lpush(key24, [field + "1", field + "2"]); + responseData.push(["lpush(key22, [1, 2])", 2]); + baseTransaction.lmpop([key24], ListDirection.LEFT); + responseData.push([ + "lmpop([key22], ListDirection.LEFT)", + { [key24]: [field + "2"] }, + ]); + baseTransaction.lpush(key24, [field + "2"]); + responseData.push(["lpush(key22, [2])", 2]); + baseTransaction.blmpop([key24], ListDirection.LEFT, 0.1, 1); + responseData.push([ + "blmpop([key22], ListDirection.LEFT, 0.1, 1)", + { [key24]: [field + "2"] }, + ]); + } + baseTransaction.lpop(key5); responseData.push(["lpop(key5)", field + "4"]); baseTransaction.llen(key5); From ed7c26df2c92f37c87efae3a84dde59f07583ab1 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 1 Aug 2024 17:46:44 -0700 Subject: [PATCH 133/236] change test name Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index db68d0a9eb..332497db22 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2807,7 +2807,7 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `expiretime and pexpiretime test_%p`, + `expireTime, pexpireTime, expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout test_%p`, async (protocol) => { await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) { From b1431d4101231f47e5fb2919bff16866e423af8c Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 11:49:18 -0700 Subject: [PATCH 134/236] fix shared test Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 78 +++++++++------------------------------ 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 332497db22..d2f7dacb9c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2688,6 +2688,12 @@ export function runBaseTests(config: { ExpireOptions.HasExistingExpiry, ), ).toEqual(true); + expect(await client.expiretime(key)).toBeGreaterThan( + Math.floor(Date.now() / 1000), + ); + expect(await client.pexpiretime(key)).toBeGreaterThan( + Date.now(), + ); } expect(await client.ttl(key)).toBeLessThanOrEqual(15); @@ -2751,7 +2757,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.ttl(key)).toEqual(-1); @@ -2769,6 +2775,13 @@ export function runBaseTests(config: { ).toEqual(true); expect(await client.ttl(key)).toEqual(-2); expect(await client.set(key, "foo")).toEqual("OK"); + + // no timeout set yet + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expiretime(key)).toEqual(-1); + expect(await client.pexpiretime(key)).toEqual(-1); + } + expect( await client.pexpireAt( key, @@ -2784,7 +2797,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expire, pexpire, expireAt, pexpireAt and ttl with non-existing key_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); expect(await client.expire(key, 10)).toEqual(false); expect(await client.pexpire(key, 10000)).toEqual(false); @@ -2801,68 +2814,11 @@ export function runBaseTests(config: { ), ).toEqual(false); expect(await client.ttl(key)).toEqual(-2); - }, protocol); - }, - config.timeout, - ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `expireTime, pexpireTime, expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout test_%p`, - async (protocol) => { - await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) { - return; + expect(await client.expiretime(key)).toEqual(-2); + expect(await client.pexpiretime(key)).toEqual(-2); } - - const key = uuidv4(); - - expect(await client.set(key, "foo")).toEqual("OK"); - expect(await client.ttl(key)).toEqual(-1); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expiretime(key)).toEqual(-1); - expect(await client.pexpiretime(key)).toEqual(-1); - } - - expect(await client.expire(key, 10)).toEqual(true); - expect(await client.ttl(key)).toBeLessThanOrEqual(10); - - // set command clears the timeout. - expect(await client.set(key, "bar")).toEqual("OK"); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.pexpire(key, 10000)).toEqual(true); - } else { - expect( - await client.pexpire( - key, - 10000, - ExpireOptions.HasNoExpiry, - ), - ).toEqual(true); - } - - expect(await client.ttl(key)).toBeLessThanOrEqual(10000); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expire(key, 15000)).toEqual(true); - } else { - expect( - await client.pexpire( - key, - 15000, - ExpireOptions.HasNoExpiry, - ), - ).toEqual(false); - expect(await client.expiretime(key)).toBeGreaterThan( - Math.floor(Date.now() / 1000), - ); - expect(await client.pexpiretime(key)).toBeGreaterThan( - Date.now(), - ); - } - - expect(await client.ttl(key)).toBeLessThan(15000); }, protocol); }, config.timeout, From 8cd4620b07177b6e3cb2f21dcf266d71b8e803ab Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:02:27 -0700 Subject: [PATCH 135/236] address new comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 2 +- node/src/Transaction.ts | 4 + node/tests/RedisClusterClient.test.ts | 985 ++++++++++++++++++++++++++ node/tests/SharedTests.ts | 4 +- 4 files changed, 992 insertions(+), 3 deletions(-) create mode 100644 node/tests/RedisClusterClient.test.ts diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1a2d9d12a3..bb5c9ce8ef 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2436,7 +2436,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Redis version 7.0.0. + * since - Valkey version 7.0.0. * * @example * ```typescript diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7d544ebd87..8a301c053a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1341,6 +1341,8 @@ export class BaseTransaction> { * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * since Valkey version 7.0.0. */ public expireTime(key: string): T { return this.addAndReturn(createExpireTime(key)); @@ -1398,6 +1400,8 @@ export class BaseTransaction> { * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * since Valkey version 7.0.0. */ public pexpireTime(key: string): T { return this.addAndReturn(createPExpireTime(key)); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts new file mode 100644 index 0000000000..7d25ddc8fe --- /dev/null +++ b/node/tests/RedisClusterClient.test.ts @@ -0,0 +1,985 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; +import { gte } from "semver"; +import { v4 as uuidv4 } from "uuid"; +import { + BitwiseOperation, + ClusterTransaction, + FunctionListResponse, + GlideClusterClient, + InfoOptions, + ListDirection, + ProtocolVersion, + Routes, + ScoreFilter, +} from ".."; +import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode } from "../build-ts/src/Commands"; +import { runBaseTests } from "./SharedTests"; +import { + checkClusterResponse, + checkFunctionListResponse, + flushAndCloseClient, + generateLuaLibCode, + getClientConfigurationOption, + getFirstResult, + intoArray, + intoString, + parseCommandLineArgs, + parseEndpoints, + transactionTest, + validateTransactionResponse, +} from "./TestUtilities"; +type Context = { + client: GlideClusterClient; +}; + +const TIMEOUT = 50000; + +describe("GlideClusterClient", () => { + let testsFailed = 0; + let cluster: RedisCluster; + let client: GlideClusterClient; + beforeAll(async () => { + const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; + // Connect to cluster or create a new one based on the parsed addresses + cluster = clusterAddresses + ? await RedisCluster.initFromExistingCluster( + parseEndpoints(clusterAddresses), + ) + : // setting replicaCount to 1 to facilitate tests routed to replicas + await RedisCluster.createCluster(true, 3, 1); + }, 20000); + + afterEach(async () => { + await flushAndCloseClient(true, cluster.getAddresses(), client); + }); + + afterAll(async () => { + if (testsFailed === 0) { + await cluster.close(); + } + }); + + runBaseTests({ + init: async (protocol, clientName?) => { + const options = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + options.protocol = protocol; + options.clientName = clientName; + testsFailed += 1; + client = await GlideClusterClient.createClient(options); + return { + context: { + client, + }, + client, + cluster, + }; + }, + close: (context: Context, testSucceeded: boolean) => { + if (testSucceeded) { + testsFailed -= 1; + } + }, + timeout: TIMEOUT, + }); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `info with server and replication_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const info_server = getFirstResult( + await client.info([InfoOptions.Server]), + ); + expect(intoString(info_server)).toEqual( + expect.stringContaining("# Server"), + ); + + const infoReplicationValues = Object.values( + await client.info([InfoOptions.Replication]), + ); + + const replicationInfo = intoArray(infoReplicationValues); + + for (const item of replicationInfo) { + expect(item).toContain("role:master"); + expect(item).toContain("# Replication"); + } + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `info with server and randomNode route_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const result = await client.info( + [InfoOptions.Server], + "randomNode", + ); + expect(intoString(result)).toEqual( + expect.stringContaining("# Server"), + ); + expect(intoString(result)).toEqual( + expect.not.stringContaining("# Errorstats"), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `route by address reaches correct node_%p`, + async (protocol) => { + // returns the line that contains the word "myself", up to that point. This is done because the values after it might change with time. + const cleanResult = (value: string) => { + return ( + value + .split("\n") + .find((line: string) => line.includes("myself")) + ?.split("myself")[0] ?? "" + ); + }; + + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const result = cleanResult( + intoString( + await client.customCommand( + ["cluster", "nodes"], + "randomNode", + ), + ), + ); + + // check that routing without explicit port works + const host = result.split(" ")[1].split("@")[0] ?? ""; + + if (!host) { + throw new Error("No host could be parsed"); + } + + const secondResult = cleanResult( + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host, + }), + ), + ); + + expect(result).toEqual(secondResult); + + const [host2, port] = host.split(":"); + + // check that routing with explicit port works + const thirdResult = cleanResult( + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host: host2, + port: Number(port), + }), + ), + ); + + expect(result).toEqual(thirdResult); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `fail routing by address if no port is provided_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + expect(() => + client.info(undefined, { + type: "routeByAddress", + host: "foo", + }), + ).toThrowError(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `config get and config set transactions test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + transaction.configSet({ timeout: "1000" }); + transaction.configGet(["timeout"]); + const result = await client.exec(transaction); + expect(intoString(result)).toEqual( + intoString(["OK", { timeout: "1000" }]), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can send transactions_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + const expectedRes = await transactionTest( + transaction, + cluster.getVersion(), + ); + const result = await client.exec(transaction); + validateTransactionResponse(result, expectedRes); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can return null on WATCH transaction failures_%p`, + async (protocol) => { + const client1 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const client2 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + transaction.get("key"); + const result1 = await client1.customCommand(["WATCH", "key"]); + expect(result1).toEqual("OK"); + + const result2 = await client2.set("key", "foo"); + expect(result2).toEqual("OK"); + + const result3 = await client1.exec(transaction); + expect(result3).toBeNull(); + + client1.close(); + client2.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `echo with all nodes routing_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const message = uuidv4(); + const echoDict = await client.echo(message, "allNodes"); + + expect(typeof echoDict).toBe("object"); + expect(intoArray(echoDict)).toEqual( + expect.arrayContaining(intoArray([message])), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command returns a cross slot error`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const promises: Promise[] = [ + client.blpop(["abc", "zxy", "lkn"], 0.1), + client.rename("abc", "zxy"), + client.msetnx({ abc: "xyz", def: "abc", hij: "def" }), + client.brpop(["abc", "zxy", "lkn"], 0.1), + client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), + client.smove("abc", "zxy", "value"), + client.renamenx("abc", "zxy"), + client.sinter(["abc", "zxy", "lkn"]), + client.sinterstore("abc", ["zxy", "lkn"]), + client.zinterstore("abc", ["zxy", "lkn"]), + client.sunionstore("abc", ["zxy", "lkn"]), + client.sunion(["abc", "zxy", "lkn"]), + client.pfcount(["abc", "zxy", "lkn"]), + client.pfmerge("abc", ["def", "ghi"]), + client.sdiff(["abc", "zxy", "lkn"]), + client.sdiffstore("abc", ["zxy", "lkn"]), + ]; + + if (gte(cluster.getVersion(), "6.2.0")) { + promises.push( + client.blmove( + "abc", + "def", + ListDirection.LEFT, + ListDirection.LEFT, + 0.2, + ), + client.zdiff(["abc", "zxy", "lkn"]), + client.zdiffWithScores(["abc", "zxy", "lkn"]), + client.zdiffstore("abc", ["zxy", "lkn"]), + client.copy("abc", "zxy", true), + ); + } + + if (gte(cluster.getVersion(), "7.0.0")) { + promises.push( + client.sintercard(["abc", "zxy", "lkn"]), + client.zintercard(["abc", "zxy", "lkn"]), + client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), + client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), + client.lcs("abc", "xyz"), + client.lcsLen("abc", "xyz"), + client.lcsIdx("abc", "xyz"), + ); + } + + for (const promise of promises) { + await expect(promise).rejects.toThrowError(/crossslot/i); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command routed to multiple nodes`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + await client.exists(["abc", "zxy", "lkn"]); + await client.unlink(["abc", "zxy", "lkn"]); + await client.del(["abc", "zxy", "lkn"]); + await client.mget(["abc", "zxy", "lkn"]); + await client.mset({ abc: "1", zxy: "2", lkn: "3" }); + await client.touch(["abc", "zxy", "lkn"]); + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object freq transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + [maxmemoryPolicyKey]: "allkeys-lfu", + }); + transaction.set(key, "foo"); + transaction.objectFreq(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + expect(response[0]).toEqual("OK"); + expect(response[1]).toEqual("OK"); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object idletime transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + // OBJECT IDLETIME requires a non-LFU maxmemory-policy + [maxmemoryPolicyKey]: "allkeys-random", + }); + transaction.set(key, "foo"); + transaction.objectIdletime(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); + expect(response[0]).toEqual("OK"); + // transaction.set(key, "foo"); + expect(response[1]).toEqual("OK"); + // transaction.objectIdletime(key); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object refcount transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const transaction = new ClusterTransaction(); + transaction.set(key, "foo"); + transaction.objectRefcount(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(2); + expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); + expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); + } + + 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, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "copy test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const source = `{key}-${uuidv4()}`; + const destination = `{key}-${uuidv4()}`; + const value1 = uuidv4(); + const value2 = uuidv4(); + + // neither key exists + expect(await client.copy(source, destination, true)).toEqual(false); + expect(await client.copy(source, destination)).toEqual(false); + + // source exists, destination does not + expect(await client.set(source, value1)).toEqual("OK"); + expect(await client.copy(source, destination, false)).toEqual(true); + expect(await client.get(destination)).toEqual(value1); + + // new value for source key + expect(await client.set(source, value2)).toEqual("OK"); + + // both exists, no REPLACE + expect(await client.copy(source, destination)).toEqual(false); + expect(await client.copy(source, destination, false)).toEqual( + false, + ); + expect(await client.get(destination)).toEqual(value1); + + // both exists, with REPLACE + expect(await client.copy(source, destination, true)).toEqual(true); + expect(await client.get(destination)).toEqual(value2); + + //transaction tests + const transaction = new ClusterTransaction(); + transaction.set(source, value1); + transaction.copy(source, destination, true); + transaction.get(destination); + const results = await client.exec(transaction); + + expect(results).toEqual(["OK", true, value1]); + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "flushdb flushall dbsize test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + expect(await client.dbsize()).toBeGreaterThanOrEqual(0); + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toBeGreaterThan(0); + + expect(await client.flushall()).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + client.close(); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function load and function list", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + let functionList = await client.functionList( + { libNamePattern: libName }, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + expect(await client.functionLoad(code)).toEqual( + libName, + ); + + functionList = await client.functionList( + { libNamePattern: libName }, + route, + ); + let expectedDescription = new Map< + string, + string | null + >([[funcName, null]]); + let expectedFlags = new Map([ + [funcName, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + ), + ); + + // call functions from that library to confirm that it works + let fcall = await client.fcallWithRoute( + funcName, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual("one"), + ); + fcall = await client.fcallReadonlyWithRoute( + funcName, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual("one"), + ); + + // re-load library without replace + await expect( + client.functionLoad(code), + ).rejects.toThrow( + `Library '${libName}' already exists`, + ); + + // re-load library with replace + expect( + await client.functionLoad(code, true), + ).toEqual(libName); + + // overwrite lib with new code + const func2Name = + "myfunc2c" + uuidv4().replaceAll("-", ""); + const newCode = generateLuaLibCode( + libName, + new Map([ + [funcName, "return args[1]"], + [func2Name, "return #args"], + ]), + true, + ); + expect( + await client.functionLoad(newCode, true), + ).toEqual(libName); + + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); + expectedDescription = new Map< + string, + string | null + >([ + [funcName, null], + [func2Name, null], + ]); + expectedFlags = new Map([ + [funcName, ["no-writes"]], + [func2Name, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + newCode, + ), + ); + + fcall = await client.fcallWithRoute( + func2Name, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + + fcall = await client.fcallReadonlyWithRoute( + func2Name, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function flush", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + const functionList1 = await client.functionList( + {}, + route, + ); + checkClusterResponse( + functionList1 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // load the library + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // flush functions + expect( + await client.functionFlush( + FlushMode.SYNC, + route, + ), + ).toEqual("OK"); + expect( + await client.functionFlush( + FlushMode.ASYNC, + route, + ), + ).toEqual("OK"); + + const functionList2 = + await client.functionList(); + checkClusterResponse( + functionList2 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Attempt to re-load library without overwriting to ensure FLUSH was effective + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function delete", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + let functionList = await client.functionList( + {}, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // Delete the function + expect( + await client.functionDelete(libName, route), + ).toEqual("OK"); + + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Delete a non-existing library + await expect( + client.functionDelete(libName, route), + ).rejects.toThrow(`Library not found`); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); +}); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d2f7dacb9c..334d6acfa6 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2777,7 +2777,7 @@ export function runBaseTests(config: { expect(await client.set(key, "foo")).toEqual("OK"); // no timeout set yet - if (cluster.checkIfServerVersionLessThan("7.0.0")) { + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { expect(await client.expiretime(key)).toEqual(-1); expect(await client.pexpiretime(key)).toEqual(-1); } @@ -2815,7 +2815,7 @@ export function runBaseTests(config: { ).toEqual(false); expect(await client.ttl(key)).toEqual(-2); - if (cluster.checkIfServerVersionLessThan("7.0.0")) { + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { expect(await client.expiretime(key)).toEqual(-2); expect(await client.pexpiretime(key)).toEqual(-2); } From cb28adcab8692da0bcb44a4f44e967dea2815a67 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:26:55 -0700 Subject: [PATCH 136/236] change the since version Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index bb5c9ce8ef..ebbaddb346 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2436,7 +2436,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Valkey version 7.0.0. + * since Valkey version 7.0.0. * * @example * ```typescript @@ -2522,7 +2522,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Redis version 7.0.0. + * since Valkey version 7.0.0. * * @example * ```typescript From eb0bea4e770328f42915f328d5f96701c66cfd51 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:28:48 -0700 Subject: [PATCH 137/236] remove redis cluster test file Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 985 -------------------------- 1 file changed, 985 deletions(-) delete mode 100644 node/tests/RedisClusterClient.test.ts diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts deleted file mode 100644 index 7d25ddc8fe..0000000000 --- a/node/tests/RedisClusterClient.test.ts +++ /dev/null @@ -1,985 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; -import { gte } from "semver"; -import { v4 as uuidv4 } from "uuid"; -import { - BitwiseOperation, - ClusterTransaction, - FunctionListResponse, - GlideClusterClient, - InfoOptions, - ListDirection, - ProtocolVersion, - Routes, - ScoreFilter, -} from ".."; -import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; -import { runBaseTests } from "./SharedTests"; -import { - checkClusterResponse, - checkFunctionListResponse, - flushAndCloseClient, - generateLuaLibCode, - getClientConfigurationOption, - getFirstResult, - intoArray, - intoString, - parseCommandLineArgs, - parseEndpoints, - transactionTest, - validateTransactionResponse, -} from "./TestUtilities"; -type Context = { - client: GlideClusterClient; -}; - -const TIMEOUT = 50000; - -describe("GlideClusterClient", () => { - let testsFailed = 0; - let cluster: RedisCluster; - let client: GlideClusterClient; - beforeAll(async () => { - const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; - // Connect to cluster or create a new one based on the parsed addresses - cluster = clusterAddresses - ? await RedisCluster.initFromExistingCluster( - parseEndpoints(clusterAddresses), - ) - : // setting replicaCount to 1 to facilitate tests routed to replicas - await RedisCluster.createCluster(true, 3, 1); - }, 20000); - - afterEach(async () => { - await flushAndCloseClient(true, cluster.getAddresses(), client); - }); - - afterAll(async () => { - if (testsFailed === 0) { - await cluster.close(); - } - }); - - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ); - options.protocol = protocol; - options.clientName = clientName; - testsFailed += 1; - client = await GlideClusterClient.createClient(options); - return { - context: { - client, - }, - client, - cluster, - }; - }, - close: (context: Context, testSucceeded: boolean) => { - if (testSucceeded) { - testsFailed -= 1; - } - }, - timeout: TIMEOUT, - }); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `info with server and replication_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const info_server = getFirstResult( - await client.info([InfoOptions.Server]), - ); - expect(intoString(info_server)).toEqual( - expect.stringContaining("# Server"), - ); - - const infoReplicationValues = Object.values( - await client.info([InfoOptions.Replication]), - ); - - const replicationInfo = intoArray(infoReplicationValues); - - for (const item of replicationInfo) { - expect(item).toContain("role:master"); - expect(item).toContain("# Replication"); - } - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `info with server and randomNode route_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const result = await client.info( - [InfoOptions.Server], - "randomNode", - ); - expect(intoString(result)).toEqual( - expect.stringContaining("# Server"), - ); - expect(intoString(result)).toEqual( - expect.not.stringContaining("# Errorstats"), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `route by address reaches correct node_%p`, - async (protocol) => { - // returns the line that contains the word "myself", up to that point. This is done because the values after it might change with time. - const cleanResult = (value: string) => { - return ( - value - .split("\n") - .find((line: string) => line.includes("myself")) - ?.split("myself")[0] ?? "" - ); - }; - - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const result = cleanResult( - intoString( - await client.customCommand( - ["cluster", "nodes"], - "randomNode", - ), - ), - ); - - // check that routing without explicit port works - const host = result.split(" ")[1].split("@")[0] ?? ""; - - if (!host) { - throw new Error("No host could be parsed"); - } - - const secondResult = cleanResult( - intoString( - await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host, - }), - ), - ); - - expect(result).toEqual(secondResult); - - const [host2, port] = host.split(":"); - - // check that routing with explicit port works - const thirdResult = cleanResult( - intoString( - await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host: host2, - port: Number(port), - }), - ), - ); - - expect(result).toEqual(thirdResult); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `fail routing by address if no port is provided_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - expect(() => - client.info(undefined, { - type: "routeByAddress", - host: "foo", - }), - ).toThrowError(); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `config get and config set transactions test_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - transaction.configSet({ timeout: "1000" }); - transaction.configGet(["timeout"]); - const result = await client.exec(transaction); - expect(intoString(result)).toEqual( - intoString(["OK", { timeout: "1000" }]), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `can send transactions_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - const expectedRes = await transactionTest( - transaction, - cluster.getVersion(), - ); - const result = await client.exec(transaction); - validateTransactionResponse(result, expectedRes); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `can return null on WATCH transaction failures_%p`, - async (protocol) => { - const client1 = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const client2 = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - transaction.get("key"); - const result1 = await client1.customCommand(["WATCH", "key"]); - expect(result1).toEqual("OK"); - - const result2 = await client2.set("key", "foo"); - expect(result2).toEqual("OK"); - - const result3 = await client1.exec(transaction); - expect(result3).toBeNull(); - - client1.close(); - client2.close(); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `echo with all nodes routing_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const message = uuidv4(); - const echoDict = await client.echo(message, "allNodes"); - - expect(typeof echoDict).toBe("object"); - expect(intoArray(echoDict)).toEqual( - expect.arrayContaining(intoArray([message])), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `check that multi key command returns a cross slot error`, - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const promises: Promise[] = [ - client.blpop(["abc", "zxy", "lkn"], 0.1), - client.rename("abc", "zxy"), - client.msetnx({ abc: "xyz", def: "abc", hij: "def" }), - client.brpop(["abc", "zxy", "lkn"], 0.1), - client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), - client.smove("abc", "zxy", "value"), - client.renamenx("abc", "zxy"), - client.sinter(["abc", "zxy", "lkn"]), - client.sinterstore("abc", ["zxy", "lkn"]), - client.zinterstore("abc", ["zxy", "lkn"]), - client.sunionstore("abc", ["zxy", "lkn"]), - client.sunion(["abc", "zxy", "lkn"]), - client.pfcount(["abc", "zxy", "lkn"]), - client.pfmerge("abc", ["def", "ghi"]), - client.sdiff(["abc", "zxy", "lkn"]), - client.sdiffstore("abc", ["zxy", "lkn"]), - ]; - - if (gte(cluster.getVersion(), "6.2.0")) { - promises.push( - client.blmove( - "abc", - "def", - ListDirection.LEFT, - ListDirection.LEFT, - 0.2, - ), - client.zdiff(["abc", "zxy", "lkn"]), - client.zdiffWithScores(["abc", "zxy", "lkn"]), - client.zdiffstore("abc", ["zxy", "lkn"]), - client.copy("abc", "zxy", true), - ); - } - - if (gte(cluster.getVersion(), "7.0.0")) { - promises.push( - client.sintercard(["abc", "zxy", "lkn"]), - client.zintercard(["abc", "zxy", "lkn"]), - client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), - client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), - client.lcs("abc", "xyz"), - client.lcsLen("abc", "xyz"), - client.lcsIdx("abc", "xyz"), - ); - } - - for (const promise of promises) { - await expect(promise).rejects.toThrowError(/crossslot/i); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `check that multi key command routed to multiple nodes`, - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - await client.exists(["abc", "zxy", "lkn"]); - await client.unlink(["abc", "zxy", "lkn"]); - await client.del(["abc", "zxy", "lkn"]); - await client.mget(["abc", "zxy", "lkn"]); - await client.mset({ abc: "1", zxy: "2", lkn: "3" }); - await client.touch(["abc", "zxy", "lkn"]); - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object freq transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const maxmemoryPolicyKey = "maxmemory-policy"; - const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); - - try { - const transaction = new ClusterTransaction(); - transaction.configSet({ - [maxmemoryPolicyKey]: "allkeys-lfu", - }); - transaction.set(key, "foo"); - transaction.objectFreq(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(3); - expect(response[0]).toEqual("OK"); - expect(response[1]).toEqual("OK"); - expect(response[2]).toBeGreaterThanOrEqual(0); - } - } finally { - expect( - await client.configSet({ - [maxmemoryPolicyKey]: maxmemoryPolicy, - }), - ).toEqual("OK"); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object idletime transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const maxmemoryPolicyKey = "maxmemory-policy"; - const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); - - try { - const transaction = new ClusterTransaction(); - transaction.configSet({ - // OBJECT IDLETIME requires a non-LFU maxmemory-policy - [maxmemoryPolicyKey]: "allkeys-random", - }); - transaction.set(key, "foo"); - transaction.objectIdletime(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(3); - // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); - expect(response[0]).toEqual("OK"); - // transaction.set(key, "foo"); - expect(response[1]).toEqual("OK"); - // transaction.objectIdletime(key); - expect(response[2]).toBeGreaterThanOrEqual(0); - } - } finally { - expect( - await client.configSet({ - [maxmemoryPolicyKey]: maxmemoryPolicy, - }), - ).toEqual("OK"); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object refcount transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const transaction = new ClusterTransaction(); - transaction.set(key, "foo"); - transaction.objectRefcount(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(2); - expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); - expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); - } - - 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, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "copy test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - if (cluster.checkIfServerVersionLessThan("6.2.0")) return; - - const source = `{key}-${uuidv4()}`; - const destination = `{key}-${uuidv4()}`; - const value1 = uuidv4(); - const value2 = uuidv4(); - - // neither key exists - expect(await client.copy(source, destination, true)).toEqual(false); - expect(await client.copy(source, destination)).toEqual(false); - - // source exists, destination does not - expect(await client.set(source, value1)).toEqual("OK"); - expect(await client.copy(source, destination, false)).toEqual(true); - expect(await client.get(destination)).toEqual(value1); - - // new value for source key - expect(await client.set(source, value2)).toEqual("OK"); - - // both exists, no REPLACE - expect(await client.copy(source, destination)).toEqual(false); - expect(await client.copy(source, destination, false)).toEqual( - false, - ); - expect(await client.get(destination)).toEqual(value1); - - // both exists, with REPLACE - expect(await client.copy(source, destination, true)).toEqual(true); - expect(await client.get(destination)).toEqual(value2); - - //transaction tests - const transaction = new ClusterTransaction(); - transaction.set(source, value1); - transaction.copy(source, destination, true); - transaction.get(destination); - const results = await client.exec(transaction); - - expect(results).toEqual(["OK", true, value1]); - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "flushdb flushall dbsize test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - expect(await client.dbsize()).toBeGreaterThanOrEqual(0); - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toBeGreaterThan(0); - - expect(await client.flushall()).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - client.close(); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function load and function list", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - - let functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - // load the library - expect(await client.functionLoad(code)).toEqual( - libName, - ); - - functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); - let expectedDescription = new Map< - string, - string | null - >([[funcName, null]]); - let expectedFlags = new Map([ - [funcName, ["no-writes"]], - ]); - - checkClusterResponse( - functionList, - singleNodeRoute, - (value) => - checkFunctionListResponse( - value as FunctionListResponse, - libName, - expectedDescription, - expectedFlags, - ), - ); - - // call functions from that library to confirm that it works - let fcall = await client.fcallWithRoute( - funcName, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual("one"), - ); - fcall = await client.fcallReadonlyWithRoute( - funcName, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual("one"), - ); - - // re-load library without replace - await expect( - client.functionLoad(code), - ).rejects.toThrow( - `Library '${libName}' already exists`, - ); - - // re-load library with replace - expect( - await client.functionLoad(code, true), - ).toEqual(libName); - - // overwrite lib with new code - const func2Name = - "myfunc2c" + uuidv4().replaceAll("-", ""); - const newCode = generateLuaLibCode( - libName, - new Map([ - [funcName, "return args[1]"], - [func2Name, "return #args"], - ]), - true, - ); - expect( - await client.functionLoad(newCode, true), - ).toEqual(libName); - - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); - expectedDescription = new Map< - string, - string | null - >([ - [funcName, null], - [func2Name, null], - ]); - expectedFlags = new Map([ - [funcName, ["no-writes"]], - [func2Name, ["no-writes"]], - ]); - - checkClusterResponse( - functionList, - singleNodeRoute, - (value) => - checkFunctionListResponse( - value as FunctionListResponse, - libName, - expectedDescription, - expectedFlags, - newCode, - ), - ); - - fcall = await client.fcallWithRoute( - func2Name, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual(2), - ); - - fcall = await client.fcallReadonlyWithRoute( - func2Name, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual(2), - ); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function flush", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - - const functionList1 = await client.functionList( - {}, - route, - ); - checkClusterResponse( - functionList1 as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // load the library - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - - // flush functions - expect( - await client.functionFlush( - FlushMode.SYNC, - route, - ), - ).toEqual("OK"); - expect( - await client.functionFlush( - FlushMode.ASYNC, - route, - ), - ).toEqual("OK"); - - const functionList2 = - await client.functionList(); - checkClusterResponse( - functionList2 as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // Attempt to re-load library without overwriting to ensure FLUSH was effective - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function delete", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - let functionList = await client.functionList( - {}, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - // load the library - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - - // Delete the function - expect( - await client.functionDelete(libName, route), - ).toEqual("OK"); - - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // Delete a non-existing library - await expect( - client.functionDelete(libName, route), - ).rejects.toThrow(`Library not found`); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); -}); From e9c27231f5534d98c020f44c5d88d11d1c14ecca Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:44:52 -0700 Subject: [PATCH 138/236] Downgrade and fix cargo-deny version (#2081) Signed-off-by: Jonathan Louie --- .github/workflows/lint-rust/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-rust/action.yml b/.github/workflows/lint-rust/action.yml index 8a7cdf185f..11ca944f71 100644 --- a/.github/workflows/lint-rust/action.yml +++ b/.github/workflows/lint-rust/action.yml @@ -42,7 +42,7 @@ runs: - run: | cargo update - cargo install cargo-deny + cargo install --locked --version 0.15.1 cargo-deny cargo deny check --config ${GITHUB_WORKSPACE}/deny.toml working-directory: ${{ inputs.cargo-toml-folder }} shell: bash From 14ebf287619b3f30c59d9b96093d6ee16b797756 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Fri, 2 Aug 2024 14:37:02 -0700 Subject: [PATCH 139/236] Node: added WATCH and UNWATCH commands (#2076) * Node: added WATCH and UNWATCH commands Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 37 ++++++++- node/src/Commands.ts | 10 +++ node/src/GlideClient.ts | 23 +++++- node/src/GlideClusterClient.ts | 25 +++++- node/tests/GlideClient.test.ts | 112 +++++++++++++++++++++++++- node/tests/GlideClusterClient.test.ts | 105 +++++++++++++++++++++++- 7 files changed, 306 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d81b0401..a679780281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) +* Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index c04d118091..97cfcce3dc 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -143,6 +143,7 @@ import { createTouch, createType, createUnlink, + createWatch, createXAdd, createXDel, createXLen, @@ -172,8 +173,8 @@ import { createZRemRangeByScore, createZRevRank, createZRevRankWithScore, - createZScore, createZScan, + createZScore, } from "./Commands"; import { ClosingError, @@ -4484,10 +4485,42 @@ export class BaseClient { * console.log(result); // Output: 2 - The last access time of 2 keys has been updated. * ``` */ - public touch(keys: string[]): Promise { + public async touch(keys: string[]): Promise { return this.createWritePromise(createTouch(keys)); } + /** + * Marks the given keys to be watched for conditional execution of a transaction. Transactions + * will only execute commands if the watched keys are not modified before execution of the + * transaction. Executing a transaction will automatically flush all previously watched keys. + * + * See https://valkey.io/commands/watch/ and https://valkey.io/topics/transactions/#cas for more details. + * + * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + * @param keys - The keys to watch. + * @returns A simple "OK" response. + * + * @example + * ```typescript + * const response = await client.watch(["sampleKey"]); + * console.log(response); // Output: "OK" + * const transaction = new Transaction().set("SampleKey", "foobar"); + * const result = await client.exec(transaction); + * console.log(result); // Output: "OK" - Executes successfully and keys are unwatched. + * ``` + * ```typescript + * const response = await client.watch(["sampleKey"]); + * console.log(response); // Output: "OK" + * const transaction = new Transaction().set("SampleKey", "foobar"); + * await client.set("sampleKey", "hello world"); + * const result = await client.exec(transaction); + * console.log(result); // Output: null - null is returned when the watched key is modified before transaction execution. + * ``` + */ + public async watch(keys: string[]): Promise<"OK"> { + return this.createWritePromise(createWatch(keys)); + } + /** * Overwrites part of the string stored at `key`, starting at the specified `offset`, * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 5416883d9d..8fb9d53b24 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3013,6 +3013,16 @@ export function createRandomKey(): command_request.Command { return createCommand(RequestType.RandomKey, []); } +/** @internal */ +export function createWatch(keys: string[]): command_request.Command { + return createCommand(RequestType.Watch, keys); +} + +/** @internal */ +export function createUnWatch(): command_request.Command { + return createCommand(RequestType.UnWatch, []); +} + /** * This base class represents the common set of optional arguments for the SCAN family of commands. * Concrete implementations of this class are tied to specific SCAN commands (SCAN, HSCAN, SSCAN, diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 582b9cd3b5..1c62b34506 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -43,6 +43,7 @@ import { createSort, createSortReadOnly, createTime, + createUnWatch, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; @@ -742,7 +743,27 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: "key12" - "key12" is a random existing key name from the currently selected database. * ``` */ - public randomKey(): Promise { + public async randomKey(): Promise { return this.createWritePromise(createRandomKey()); } + + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys. + * + * See https://valkey.io/commands/unwatch/ and https://valkey.io/topics/transactions/#cas for more details. + * + * @returns A simple "OK" response. + * + * @example + * ```typescript + * let response = await client.watch(["sampleKey"]); + * console.log(response); // Output: "OK" + * response = await client.unwatch(); + * console.log(response); // Output: "OK" + * ``` + */ + public async unwatch(): Promise<"OK"> { + return this.createWritePromise(createUnWatch()); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 8f1c9ad99c..0cc34f0b8a 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -44,6 +44,7 @@ import { createSort, createSortReadOnly, createTime, + createUnWatch, } from "./Commands"; import { RequestError } from "./Errors"; import { command_request, connection_request } from "./ProtobufMessage"; @@ -1117,10 +1118,32 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: "key12" - "key12" is a random existing key name. * ``` */ - public randomKey(route?: Routes): Promise { + public async randomKey(route?: Routes): Promise { return this.createWritePromise( createRandomKey(), toProtobufRoute(route), ); } + + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys. + * + * See https://valkey.io/commands/unwatch/ and https://valkey.io/topics/transactions/#cas for more details. + * + * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, + * in which case the client will route the command to the nodes defined by `route`. + * @returns A simple "OK" response. + * + * @example + * ```typescript + * let response = await client.watch(["sampleKey"]); + * console.log(response); // Output: "OK" + * response = await client.unwatch(); + * console.log(response); // Output: "OK" + * ``` + */ + public async unwatch(route?: Routes): Promise<"OK"> { + return this.createWritePromise(createUnWatch(), toProtobufRoute(route)); + } } diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 8893c81cf2..11b75ef82e 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -12,7 +12,13 @@ import { } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; -import { GlideClient, ListDirection, ProtocolVersion, Transaction } from ".."; +import { + GlideClient, + ListDirection, + ProtocolVersion, + RequestError, + Transaction, +} from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; @@ -227,7 +233,7 @@ describe("GlideClient", () => { ); const transaction = new Transaction(); transaction.get("key"); - const result1 = await client1.customCommand(["WATCH", "key"]); + const result1 = await client1.watch(["key"]); expect(result1).toEqual("OK"); const result2 = await client2.set("key", "foo"); @@ -939,6 +945,108 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "watch test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + const key4 = "{key}-4" + uuidv4(); + const setFoobarTransaction = new Transaction(); + const setHelloTransaction = new Transaction(); + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.set(key2, "hello")).toEqual("OK"); + setFoobarTransaction + .set(key1, "foobar") + .set(key2, "foobar") + .set(key3, "foobar"); + let results = await client.exec(setFoobarTransaction); + expect(results).toEqual(null); + // sanity check + expect(await client.get(key1)).toEqual(null); + expect(await client.get(key2)).toEqual("hello"); + expect(await client.get(key3)).toEqual(null); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.get(key2)).toEqual("hello"); + results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + expect(await client.get(key3)).toEqual("foobar"); + + // Transaction executes command successfully with unmodified watched keys + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + expect(await client.get(key3)).toEqual("foobar"); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + expect(await client.watch([key4])).toEqual("OK"); + setHelloTransaction + .set(key1, "hello") + .set(key2, "hello") + .set(key3, "hello"); + results = await client.exec(setHelloTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("hello"); + expect(await client.get(key2)).toEqual("hello"); + expect(await client.get(key3)).toEqual("hello"); + + // WATCH can not have an empty String array parameter + await expect(client.watch([])).rejects.toThrow(RequestError); + + client.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "unwatch test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + + const setFoobarTransaction = new Transaction(); + + // UNWATCH returns OK when there no watched keys + expect(await client.unwatch()).toEqual("OK"); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + expect(await client.watch([key1, key2])).toEqual("OK"); + expect(await client.set(key2, "hello")).toEqual("OK"); + expect(await client.unwatch()).toEqual("OK"); + setFoobarTransaction.set(key1, "foobar").set(key2, "foobar"); + const results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + + client.close(); + }, + TIMEOUT, + ); + runBaseTests({ init: async (protocol, clientName?) => { const options = getClientConfigurationOption( diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 19d7b91c7b..7b53e16edd 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -274,7 +274,7 @@ describe("GlideClusterClient", () => { ); const transaction = new ClusterTransaction(); transaction.get("key"); - const result1 = await client1.customCommand(["WATCH", "key"]); + const result1 = await client1.watch(["key"]); expect(result1).toEqual("OK"); const result2 = await client2.set("key", "foo"); @@ -385,6 +385,7 @@ describe("GlideClusterClient", () => { await client.mget(["abc", "zxy", "lkn"]); await client.mset({ abc: "1", zxy: "2", lkn: "3" }); await client.touch(["abc", "zxy", "lkn"]); + await client.watch(["ghi", "zxy", "lkn"]); client.close(); }, ); @@ -1110,4 +1111,106 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "watch test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + const key4 = "{key}-4" + uuidv4(); + const setFoobarTransaction = new ClusterTransaction(); + const setHelloTransaction = new ClusterTransaction(); + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.set(key2, "hello")).toEqual("OK"); + setFoobarTransaction + .set(key1, "foobar") + .set(key2, "foobar") + .set(key3, "foobar"); + let results = await client.exec(setFoobarTransaction); + expect(results).toEqual(null); + // sanity check + expect(await client.get(key1)).toEqual(null); + expect(await client.get(key2)).toEqual("hello"); + expect(await client.get(key3)).toEqual(null); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.get(key2)).toEqual("hello"); + results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + expect(await client.get(key3)).toEqual("foobar"); + + // Transaction executes command successfully with unmodified watched keys + expect(await client.watch([key1, key2, key3])).toEqual("OK"); + results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + expect(await client.get(key3)).toEqual("foobar"); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + expect(await client.watch([key4])).toEqual("OK"); + setHelloTransaction + .set(key1, "hello") + .set(key2, "hello") + .set(key3, "hello"); + results = await client.exec(setHelloTransaction); + expect(results).toEqual(["OK", "OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("hello"); + expect(await client.get(key2)).toEqual("hello"); + expect(await client.get(key3)).toEqual("hello"); + + // WATCH can not have an empty String array parameter + await expect(client.watch([])).rejects.toThrow(RequestError); + + client.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "unwatch test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const setFoobarTransaction = new ClusterTransaction(); + + // UNWATCH returns OK when there no watched keys + expect(await client.unwatch()).toEqual("OK"); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + expect(await client.watch([key1, key2])).toEqual("OK"); + expect(await client.set(key2, "hello")).toEqual("OK"); + expect(await client.unwatch()).toEqual("OK"); + expect(await client.unwatch("allPrimaries")).toEqual("OK"); + setFoobarTransaction.set(key1, "foobar").set(key2, "foobar"); + const results = await client.exec(setFoobarTransaction); + expect(results).toEqual(["OK", "OK"]); + // sanity check + expect(await client.get(key1)).toEqual("foobar"); + expect(await client.get(key2)).toEqual("foobar"); + + client.close(); + }, + TIMEOUT, + ); }); From 5eae0ec6c96be7f66b76385fa8898da5013a2fa9 Mon Sep 17 00:00:00 2001 From: barshaul Date: Sun, 4 Aug 2024 12:25:46 +0000 Subject: [PATCH 140/236] Python: Export PubSubMsg Signed-off-by: barshaul --- python/python/glide/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index f1f758aeed..50caeb5f4a 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -20,6 +20,7 @@ from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, + CoreCommands, ExpireOptions, ExpiryGetEx, ExpirySet, @@ -101,6 +102,8 @@ from .glide import ClusterScanCursor, Script +PubSubMsg = CoreCommands.PubSubMsg + __all__ = [ # Client "GlideClient", @@ -180,6 +183,8 @@ "TrimByMinId", "UpdateOptions", "ClusterScanCursor" + # PubSub + "PubSubMsg", # Logger "Logger", "LogLevel", From ad645e2d2c8b065a3559e6c24bf18c52036bce71 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Sun, 4 Aug 2024 00:23:27 +0000 Subject: [PATCH 141/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 64 ++----------------------- java/THIRD_PARTY_LICENSES_JAVA | 64 ++----------------------- node/THIRD_PARTY_LICENSES_NODE | 70 ++++------------------------ python/THIRD_PARTY_LICENSES_PYTHON | 68 +++------------------------ 4 files changed, 25 insertions(+), 241 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 127c59e8ce..b40256be0f 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.7.0 +Package: bytes:1.7.1 The following copyrights and licenses were found in the source code of this package: @@ -6809,7 +6809,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.30 +Package: flate2:1.0.31 The following copyrights and licenses were found in the source code of this package: @@ -11649,7 +11649,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: integer-encoding:4.0.0 +Package: integer-encoding:4.0.2 The following copyrights and licenses were found in the source code of this package: @@ -17346,7 +17346,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.18 +Package: ppv-lite86:0.2.20 The following copyrights and licenses were found in the source code of this package: @@ -20024,7 +20024,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.2 +Package: rustls-pemfile:2.1.3 The following copyrights and licenses were found in the source code of this package: @@ -31421,33 +31421,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -31700,33 +31673,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy-derive:0.7.35 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 31f04702ac..18554d333a 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.7.0 +Package: bytes:1.7.1 The following copyrights and licenses were found in the source code of this package: @@ -7038,7 +7038,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.30 +Package: flate2:1.0.31 The following copyrights and licenses were found in the source code of this package: @@ -12086,7 +12086,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: integer-encoding:4.0.0 +Package: integer-encoding:4.0.2 The following copyrights and licenses were found in the source code of this package: @@ -18241,7 +18241,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.18 +Package: ppv-lite86:0.2.20 The following copyrights and licenses were found in the source code of this package: @@ -20919,7 +20919,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.2 +Package: rustls-pemfile:2.1.3 The following copyrights and licenses were found in the source code of this package: @@ -34377,33 +34377,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -34656,33 +34629,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy-derive:0.7.35 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 68538b3501..e035208e1f 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -3555,7 +3555,7 @@ For more information, please refer to ---- -Package: bytes:1.7.0 +Package: bytes:1.7.1 The following copyrights and licenses were found in the source code of this package: @@ -7115,7 +7115,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.30 +Package: flate2:1.0.31 The following copyrights and licenses were found in the source code of this package: @@ -12163,7 +12163,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: integer-encoding:4.0.0 +Package: integer-encoding:4.0.2 The following copyrights and licenses were found in the source code of this package: @@ -18003,7 +18003,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.18 +Package: ppv-lite86:0.2.20 The following copyrights and licenses were found in the source code of this package: @@ -19966,7 +19966,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: regex:1.10.5 +Package: regex:1.10.6 The following copyrights and licenses were found in the source code of this package: @@ -21368,7 +21368,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.2 +Package: rustls-pemfile:2.1.3 The following copyrights and licenses were found in the source code of this package: @@ -33681,33 +33681,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -33960,33 +33933,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -37547,7 +37493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: undici-types:6.11.1 +Package: undici-types:6.13.0 The following copyrights and licenses were found in the source code of this package: @@ -37957,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.0.2 +Package: @types:node:22.1.0 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 5d2f4dbb90..a430e5c146 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -3503,7 +3503,7 @@ For more information, please refer to ---- -Package: bytes:1.7.0 +Package: bytes:1.7.1 The following copyrights and licenses were found in the source code of this package: @@ -6809,7 +6809,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.30 +Package: flate2:1.0.31 The following copyrights and licenses were found in the source code of this package: @@ -12315,7 +12315,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: integer-encoding:4.0.0 +Package: integer-encoding:4.0.2 The following copyrights and licenses were found in the source code of this package: @@ -18266,7 +18266,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.18 +Package: ppv-lite86:0.2.20 The following copyrights and licenses were found in the source code of this package: @@ -22089,7 +22089,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.2 +Package: rustls-pemfile:2.1.3 The following copyrights and licenses were found in the source code of this package: @@ -33939,33 +33939,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -34218,33 +34191,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.6.6 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -34934,7 +34880,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: attrs:23.2.0 +Package: attrs:24.1.0 The following copyrights and licenses were found in the source code of this package: @@ -34959,7 +34905,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: black:24.4.2 +Package: black:24.8.0 The following copyrights and licenses were found in the source code of this package: From f0f86b8d8bbacf419cc7233ed22c6db212208e9d Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Mon, 5 Aug 2024 10:37:40 +0300 Subject: [PATCH 142/236] Node: add PUBSUB * commands (#2090) --------- Signed-off-by: Shoham Elias --- node/src/BaseClient.ts | 79 ++- node/src/Commands.ts | 43 ++ node/src/GlideClusterClient.ts | 53 ++ node/src/Transaction.ts | 86 ++- node/tests/GlideClusterClient.test.ts | 9 +- node/tests/PubSub.test.ts | 743 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 6 + 7 files changed, 1013 insertions(+), 6 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 12761b85e6..0ef0563708 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -47,6 +47,7 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createBLMPop, createBLMove, createBLPop, createBRPop, @@ -55,7 +56,6 @@ import { createBitField, createBitOp, createBitPos, - createBLMPop, createDecr, createDecrBy, createDel, @@ -92,8 +92,8 @@ import { createLIndex, createLInsert, createLLen, - createLMove, createLMPop, + createLMove, createLPop, createLPos, createLPush, @@ -117,6 +117,9 @@ import { createPfAdd, createPfCount, createPfMerge, + createPubSubChannels, + createPubSubNumPat, + createPubSubNumSub, createRPop, createRPush, createRPushX, @@ -4672,6 +4675,78 @@ export class BaseClient { ); } + /** + * Lists the currently active channels. + * The command is routed to all nodes, and aggregates the response to a single array. + * + * See https://valkey.io/commands/pubsub-channels for more details. + * + * @param pattern - A glob-style pattern to match active channels. + * If not provided, all active channels are returned. + * @returns A list of currently active channels matching the given pattern. + * If no pattern is specified, all active channels are returned. + * + * @example + * ```typescript + * const channels = await client.pubsubChannels(); + * console.log(channels); // Output: ["channel1", "channel2"] + * + * const newsChannels = await client.pubsubChannels("news.*"); + * console.log(newsChannels); // Output: ["news.sports", "news.weather"] + * ``` + */ + public async pubsubChannels(pattern?: string): Promise { + return this.createWritePromise(createPubSubChannels(pattern)); + } + + /** + * Returns the number of unique patterns that are subscribed to by clients. + * + * Note: This is the total number of unique patterns all the clients are subscribed to, + * not the count of clients subscribed to patterns. + * The command is routed to all nodes, and aggregates the response to the sum of all pattern subscriptions. + * + * See https://valkey.io/commands/pubsub-numpat for more details. + * + * @returns The number of unique patterns. + * + * @example + * ```typescript + * const patternCount = await client.pubsubNumpat(); + * console.log(patternCount); // Output: 3 + * ``` + */ + public async pubsubNumPat(): Promise { + return this.createWritePromise(createPubSubNumPat()); + } + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + * + * Note that it is valid to call this command without channels. In this case, it will just return an empty map. + * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + * + * See https://valkey.io/commands/pubsub-numsub for more details. + * + * @param channels - The list of channels to query for the number of subscribers. + * If not provided, returns an empty map. + * @returns A map where keys are the channel names and values are the number of subscribers. + * + * @example + * ```typescript + * const result1 = await client.pubsubNumsub(["channel1", "channel2"]); + * console.log(result1); // Output: { "channel1": 3, "channel2": 5 } + * + * const result2 = await client.pubsubNumsub(); + * console.log(result2); // Output: {} + * ``` + */ + public async pubsubNumSub( + channels?: string[], + ): Promise> { + return this.createWritePromise(createPubSubNumSub(channels)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 81573afe55..c80e5db0e3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3132,3 +3132,46 @@ export function createBLMPop( return createCommand(RequestType.BLMPop, args); } + +/** + * @internal + */ +export function createPubSubChannels( + pattern?: string, +): command_request.Command { + return createCommand(RequestType.PubSubChannels, pattern ? [pattern] : []); +} + +/** + * @internal + */ +export function createPubSubNumPat(): command_request.Command { + return createCommand(RequestType.PubSubNumPat, []); +} + +/** + * @internal + */ +export function createPubSubNumSub( + channels?: string[], +): command_request.Command { + return createCommand(RequestType.PubSubNumSub, channels ? channels : []); +} + +/** + * @internal + */ +export function createPubsubShardChannels( + pattern?: string, +): command_request.Command { + return createCommand(RequestType.PubSubSChannels, pattern ? [pattern] : []); +} + +/** + * @internal + */ +export function createPubSubShardNumSub( + channels?: string[], +): command_request.Command { + return createCommand(RequestType.PubSubSNumSub, channels ? channels : []); +} diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 0cc34f0b8a..14f5e0f7fb 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -39,7 +39,9 @@ import { createLastSave, createLolwut, createPing, + createPubSubShardNumSub, createPublish, + createPubsubShardChannels, createRandomKey, createSort, createSortReadOnly, @@ -992,6 +994,57 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Lists the currently active shard channels. + * The command is routed to all nodes, and aggregates the response to a single array. + * + * See https://valkey.io/commands/pubsub-shardchannels for more details. + * + * @param pattern - A glob-style pattern to match active shard channels. + * If not provided, all active shard channels are returned. + * @returns A list of currently active shard channels matching the given pattern. + * If no pattern is specified, all active shard channels are returned. + * + * @example + * ```typescript + * const allChannels = await client.pubsubShardchannels(); + * console.log(allChannels); // Output: ["channel1", "channel2"] + * + * const filteredChannels = await client.pubsubShardchannels("channel*"); + * console.log(filteredChannels); // Output: ["channel1", "channel2"] + * ``` + */ + public async pubsubShardChannels(pattern?: string): Promise { + return this.createWritePromise(createPubsubShardChannels(pattern)); + } + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. + * + * Note that it is valid to call this command without channels. In this case, it will just return an empty map. + * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + * + * See https://valkey.io/commands/pubsub-shardnumsub for more details. + * + * @param channels - The list of shard channels to query for the number of subscribers. + * If not provided, returns an empty map. + * @returns A map where keys are the shard channel names and values are the number of subscribers. + * + * @example + * ```typescript + * const result1 = await client.pubsubShardnumsub(["channel1", "channel2"]); + * console.log(result1); // Output: { "channel1": 3, "channel2": 5 } + * + * const result2 = await client.pubsubShardnumsub(); + * console.log(result2); // Output: {} + * ``` + */ + public async pubsubShardNumSub( + channels?: string[], + ): Promise> { + return this.createWritePromise(createPubSubShardNumSub(channels)); + } + /** * Sorts the elements in the list, set, or sorted set at `key` and returns the result. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3d41ed5e39..1009b966c1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -51,9 +51,9 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createBLMPop, createBLMove, createBLPop, - createBLMPop, createBRPop, createBZMPop, createBitCount, @@ -113,8 +113,8 @@ import { createLIndex, createLInsert, createLLen, - createLMove, createLMPop, + createLMove, createLPop, createLPos, createLPush, @@ -141,7 +141,12 @@ import { createPfCount, createPfMerge, createPing, + createPubSubChannels, + createPubSubNumPat, + createPubSubNumSub, + createPubSubShardNumSub, createPublish, + createPubsubShardChannels, createRPop, createRPush, createRPushX, @@ -2810,6 +2815,52 @@ export class BaseTransaction> { ): T { return this.addAndReturn(createBLMPop(timeout, keys, direction, count)); } + + /** + * Lists the currently active channels. + * The command is routed to all nodes, and aggregates the response to a single array. + * + * See https://valkey.io/commands/pubsub-channels for more details. + * + * @param pattern - A glob-style pattern to match active channels. + * If not provided, all active channels are returned. + * Command Response - A list of currently active channels matching the given pattern. + * If no pattern is specified, all active channels are returned. + */ + public pubsubChannels(pattern?: string): T { + return this.addAndReturn(createPubSubChannels(pattern)); + } + + /** + * Returns the number of unique patterns that are subscribed to by clients. + * + * Note: This is the total number of unique patterns all the clients are subscribed to, + * not the count of clients subscribed to patterns. + * The command is routed to all nodes, and aggregates the response to the sum of all pattern subscriptions. + * + * See https://valkey.io/commands/pubsub-numpat for more details. + * + * Command Response - The number of unique patterns. + */ + public pubsubNumPat(): T { + return this.addAndReturn(createPubSubNumPat()); + } + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + * + * Note that it is valid to call this command without channels. In this case, it will just return an empty map. + * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + * + * See https://valkey.io/commands/pubsub-numsub for more details. + * + * @param channels - The list of channels to query for the number of subscribers. + * If not provided, returns an empty map. + * Command Response - A map where keys are the channel names and values are the number of subscribers. + */ + public pubsubNumSub(channels?: string[]): T { + return this.addAndReturn(createPubSubNumSub(channels)); + } } /** @@ -3078,4 +3129,35 @@ export class ClusterTransaction extends BaseTransaction { ): ClusterTransaction { return this.addAndReturn(createPublish(message, channel, sharded)); } + + /** + * Lists the currently active shard channels. + * The command is routed to all nodes, and aggregates the response to a single array. + * + * See https://valkey.io/commands/pubsub-shardchannels for more details. + * + * @param pattern - A glob-style pattern to match active shard channels. + * If not provided, all active shard channels are returned. + * Command Response - A list of currently active shard channels matching the given pattern. + * If no pattern is specified, all active shard channels are returned. + */ + public pubsubShardChannels(pattern?: string): ClusterTransaction { + return this.addAndReturn(createPubsubShardChannels(pattern)); + } + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. + * + * Note that it is valid to call this command without channels. In this case, it will just return an empty map. + * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. + * + * See https://valkey.io/commands/pubsub-shardnumsub for more details. + * + * @param channels - The list of shard channels to query for the number of subscribers. + * If not provided, returns an empty map. + * @returns A map where keys are the shard channel names and values are the number of subscribers. + */ + public pubsubShardNumSub(channels?: string[]): ClusterTransaction { + return this.addAndReturn(createPubSubShardNumSub(channels)); + } } diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 7b53e16edd..930df79c62 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -253,8 +253,13 @@ describe("GlideClusterClient", () => { ); if (!cluster.checkIfServerVersionLessThan("7.0.0")) { - transaction.publish("message", "key"); - expectedRes.push(['publish("message", "key")', 0]); + transaction.publish("message", "key", true); + expectedRes.push(['publish("message", "key", true)', 0]); + + transaction.pubsubShardChannels(); + expectedRes.push(["pubsubShardChannels()", []]); + transaction.pubsubShardNumSub(); + expectedRes.push(["pubsubShardNumSub()", {}]); } const result = await client.exec(transaction); diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts index e7d17220d1..76560635af 100644 --- a/node/tests/PubSub.test.ts +++ b/node/tests/PubSub.test.ts @@ -3280,4 +3280,747 @@ describe("PubSub", () => { }, TIMEOUT, ); + + /** + * Tests the pubsubChannels command functionality. + * + * This test verifies that the pubsubChannels command correctly returns + * the active channels matching a specified pattern. + * + * It covers the following scenarios: + * - Checking that no channels exist initially + * - Subscribing to multiple channels + * - Retrieving all active channels without a pattern + * - Retrieving channels matching a specific pattern + * - Verifying that a non-matching pattern returns no channels + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub channels_%p", + async (clusterMode) => { + let pubSub: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + let client: TGlideClient | null = null; + + try { + const channel1 = "test_channel1"; + const channel2 = "test_channel2"; + const channel3 = "some_channel3"; + const pattern = "test_*"; + + if (clusterMode) { + client = await GlideClusterClient.createClient( + getOptions(clusterMode), + ); + } else { + client = await GlideClient.createClient( + getOptions(clusterMode), + ); + } + + // Assert no channels exists yet + expect(await client.pubsubChannels()).toEqual([]); + + pubSub = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel1, channel2, channel3]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel1, channel2, channel3]), + }, + ); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Test pubsubChannels without pattern + const channels = await client2.pubsubChannels(); + expect(new Set(channels)).toEqual( + new Set([channel1, channel2, channel3]), + ); + + // Test pubsubChannels with pattern + const channelsWithPattern = + await client2.pubsubChannels(pattern); + expect(new Set(channelsWithPattern)).toEqual( + new Set([channel1, channel2]), + ); + + // Test with non-matching pattern + const nonMatchingChannels = + await client2.pubsubChannels("non_matching_*"); + expect(nonMatchingChannels.length).toBe(0); + } finally { + if (client1) { + await clientCleanup( + client1, + clusterMode ? pubSub! : undefined, + ); + } + + if (client2) { + await clientCleanup(client2); + } + + if (client) { + await clientCleanup(client); + } + } + }, + TIMEOUT, + ); + + /** + * Tests the pubsubNumPat command functionality. + * + * This test verifies that the pubsubNumPat command correctly returns + * the number of unique patterns that are subscribed to by clients. + * + * It covers the following scenarios: + * - Checking that no patterns exist initially + * - Subscribing to multiple patterns + * - Verifying the correct number of unique patterns + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub numpat_%p", + async (clusterMode) => { + let pubSub: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + let client: TGlideClient | null = null; + + try { + const pattern1 = "test_*"; + const pattern2 = "another_*"; + + // Create a client and check initial number of patterns + if (clusterMode) { + client = await GlideClusterClient.createClient( + getOptions(clusterMode), + ); + } else { + client = await GlideClient.createClient( + getOptions(clusterMode), + ); + } + + expect(await client.pubsubNumPat()).toBe(0); + + // Set up subscriptions with patterns + pubSub = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Pattern]: new Set([pattern1, pattern2]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Pattern]: + new Set([pattern1, pattern2]), + }, + ); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + const numPatterns = await client2.pubsubNumPat(); + expect(numPatterns).toBe(2); + } finally { + if (client1) { + await clientCleanup( + client1, + clusterMode ? pubSub! : undefined, + ); + } + + if (client2) { + await clientCleanup(client2); + } + + if (client) { + await clientCleanup(client); + } + } + }, + TIMEOUT, + ); + + /** + * Tests the pubsubNumSub command functionality. + * + * This test verifies that the pubsubNumSub command correctly returns + * the number of subscribers for specified channels. + * + * It covers the following scenarios: + * - Checking that no subscribers exist initially + * - Creating multiple clients with different channel subscriptions + * - Verifying the correct number of subscribers for each channel + * - Testing pubsubNumSub with no channels specified + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true, false])( + "test pubsub numsub_%p", + async (clusterMode) => { + let pubSub1: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSub2: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let pubSub3: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + let client3: TGlideClient | null = null; + let client4: TGlideClient | null = null; + let client: TGlideClient | null = null; + + try { + const channel1 = "test_channel1"; + const channel2 = "test_channel2"; + const channel3 = "test_channel3"; + const channel4 = "test_channel4"; + + // Set up subscriptions + pubSub1 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel1, channel2, channel3]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel1, channel2, channel3]), + }, + ); + pubSub2 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel2, channel3]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel2, channel3]), + }, + ); + pubSub3 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel3]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel3]), + }, + ); + + // Create a client and check initial subscribers + if (clusterMode) { + client = await GlideClusterClient.createClient( + getOptions(clusterMode), + ); + } else { + client = await GlideClient.createClient( + getOptions(clusterMode), + ); + } + + expect( + await client.pubsubNumSub([channel1, channel2, channel3]), + ).toEqual({ + [channel1]: 0, + [channel2]: 0, + [channel3]: 0, + }); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub1, + pubSub2, + ); + [client3, client4] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub3, + ); + + // Test pubsubNumsub + const subscribers = await client2.pubsubNumSub([ + channel1, + channel2, + channel3, + channel4, + ]); + expect(subscribers).toEqual({ + [channel1]: 1, + [channel2]: 2, + [channel3]: 3, + [channel4]: 0, + }); + + // Test pubsubNumsub with no channels + const emptySubscribers = await client2.pubsubNumSub(); + expect(emptySubscribers).toEqual({}); + } finally { + if (client1) { + await clientCleanup( + client1, + clusterMode ? pubSub1! : undefined, + ); + } + + if (client2) { + await clientCleanup( + client2, + clusterMode ? pubSub2! : undefined, + ); + } + + if (client3) { + await clientCleanup( + client3, + clusterMode ? pubSub3! : undefined, + ); + } + + if (client4) { + await clientCleanup(client4); + } + + if (client) { + await clientCleanup(client); + } + } + }, + TIMEOUT, + ); + + /** + * Tests the pubsubShardchannels command functionality. + * + * This test verifies that the pubsubShardchannels command correctly returns + * the active sharded channels matching a specified pattern. + * + * It covers the following scenarios: + * - Checking that no sharded channels exist initially + * - Subscribing to multiple sharded channels + * - Retrieving all active sharded channels without a pattern + * - Retrieving sharded channels matching a specific pattern + * - Verifying that a non-matching pattern returns no channels + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub shardchannels_%p", + async (clusterMode) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + + let pubSub: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + let client: TGlideClient | null = null; + + try { + const channel1 = "test_shardchannel1"; + const channel2 = "test_shardchannel2"; + const channel3 = "some_shardchannel3"; + const pattern = "test_*"; + + client = await GlideClusterClient.createClient( + getOptions(clusterMode), + ); + + // Assert no sharded channels exist yet + expect(await client.pubsubShardChannels()).toEqual([]); + + pubSub = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel1, channel2, channel3]), + }, + {}, + ); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + // Test pubsubShardchannels without pattern + const channels = await ( + client2 as GlideClusterClient + ).pubsubShardChannels(); + expect(new Set(channels)).toEqual( + new Set([channel1, channel2, channel3]), + ); + + // Test pubsubShardchannels with pattern + const channelsWithPattern = await ( + client2 as GlideClusterClient + ).pubsubShardChannels(pattern); + expect(new Set(channelsWithPattern)).toEqual( + new Set([channel1, channel2]), + ); + + // Test with non-matching pattern + const nonMatchingChannels = await ( + client2 as GlideClusterClient + ).pubsubShardChannels("non_matching_*"); + expect(nonMatchingChannels).toEqual([]); + } finally { + if (client1) { + await clientCleanup(client1, pubSub ? pubSub : undefined); + } + + if (client2) { + await clientCleanup(client2); + } + + if (client) { + await clientCleanup(client); + } + } + }, + TIMEOUT, + ); + + /** + * Tests the pubsubShardnumsub command functionality. + * + * This test verifies that the pubsubShardnumsub command correctly returns + * the number of subscribers for specified sharded channels. + * + * It covers the following scenarios: + * - Checking that no subscribers exist initially for sharded channels + * - Creating multiple clients with different sharded channel subscriptions + * - Verifying the correct number of subscribers for each sharded channel + * - Testing pubsubShardnumsub with no channels specified + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub shardnumsub_%p", + async (clusterMode) => { + let pubSub1: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let pubSub2: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let pubSub3: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + let client3: TGlideClient | null = null; + let client4: TGlideClient | null = null; + let client: TGlideClient | null = null; + + try { + const channel1 = "test_shardchannel1"; + const channel2 = "test_shardchannel2"; + const channel3 = "test_shardchannel3"; + const channel4 = "test_shardchannel4"; + + // Set up subscriptions + pubSub1 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel1, channel2, channel3]), + }, + {}, + ); + pubSub2 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel2, channel3]), + }, + {}, + ); + pubSub3 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([channel3]), + }, + {}, + ); + + // Create a client and check initial subscribers + client = await GlideClusterClient.createClient( + getOptions(clusterMode), + ); + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + + expect( + await (client as GlideClusterClient).pubsubShardNumSub([ + channel1, + channel2, + channel3, + ]), + ).toEqual({ + [channel1]: 0, + [channel2]: 0, + [channel3]: 0, + }); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub1, + pubSub2, + ); + [client3, client4] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub3, + ); + + // Test pubsubShardnumsub + const subscribers = await ( + client4 as GlideClusterClient + ).pubsubShardNumSub([channel1, channel2, channel3, channel4]); + expect(subscribers).toEqual({ + [channel1]: 1, + [channel2]: 2, + [channel3]: 3, + [channel4]: 0, + }); + + // Test pubsubShardnumsub with no channels + const emptySubscribers = await ( + client4 as GlideClusterClient + ).pubsubShardNumSub(); + expect(emptySubscribers).toEqual({}); + } finally { + if (client1) { + await clientCleanup(client1, pubSub1 ? pubSub1 : undefined); + } + + if (client2) { + await clientCleanup(client2, pubSub2 ? pubSub2 : undefined); + } + + if (client3) { + await clientCleanup(client3, pubSub3 ? pubSub3 : undefined); + } + + if (client4) { + await clientCleanup(client4); + } + + if (client) { + await clientCleanup(client); + } + } + }, + TIMEOUT, + ); + + /** + * Tests that pubsubChannels doesn't return sharded channels and pubsubShardchannels + * doesn't return regular channels. + * + * This test verifies the separation between regular and sharded channels in PUBSUB operations. + * + * It covers the following scenarios: + * - Subscribing to both a regular channel and a sharded channel + * - Verifying that pubsubChannels only returns the regular channel + * - Verifying that pubsubShardchannels only returns the sharded channel + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub channels and shardchannels separation_%p", + async (clusterMode) => { + let pubSub: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + + try { + const regularChannel = "regular_channel"; + const shardChannel = "shard_channel"; + + pubSub = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([regularChannel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([shardChannel]), + }, + {}, + ); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub, + ); + + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + + // Test pubsubChannels + const regularChannels = await client2.pubsubChannels(); + expect(regularChannels).toEqual([regularChannel]); + + // Test pubsubShardchannels + const shardChannels = await ( + client2 as GlideClusterClient + ).pubsubShardChannels(); + expect(shardChannels).toEqual([shardChannel]); + } finally { + if (client1) { + await clientCleanup(client1, pubSub ? pubSub : undefined); + } + + if (client2) { + await clientCleanup(client2); + } + } + }, + TIMEOUT, + ); + + /** + * Tests that pubsubNumSub doesn't count sharded channel subscribers and pubsubShardnumsub + * doesn't count regular channel subscribers. + * + * This test verifies the separation between regular and sharded channel subscribers in PUBSUB operations. + * + * It covers the following scenarios: + * - Subscribing to both a regular channel and a sharded channel with two clients + * - Verifying that pubsubNumSub only counts subscribers for the regular channel + * - Verifying that pubsubShardnumsub only counts subscribers for the sharded channel + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + */ + it.each([true])( + "test pubsub numsub and shardnumsub separation_%p", + async (clusterMode) => { + let pubSub1: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let pubSub2: GlideClusterClientConfiguration.PubSubSubscriptions | null = + null; + let client1: TGlideClient | null = null; + let client2: TGlideClient | null = null; + + try { + const regularChannel = "regular_channel"; + const shardChannel = "shard_channel"; + + pubSub1 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([regularChannel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([shardChannel]), + }, + {}, + ); + pubSub2 = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([regularChannel]), + [GlideClusterClientConfiguration.PubSubChannelModes + .Sharded]: new Set([shardChannel]), + }, + {}, + ); + + [client1, client2] = await createClients( + clusterMode, + getOptions(clusterMode), + getOptions(clusterMode), + pubSub1, + pubSub2, + ); + + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + + // Test pubsubNumsub + const regularSubscribers = await client2.pubsubNumSub([ + regularChannel, + shardChannel, + ]); + expect(regularSubscribers).toEqual({ + [regularChannel]: 2, + [shardChannel]: 0, + }); + + // Test pubsubShardnumsub + const shardSubscribers = await ( + client2 as GlideClusterClient + ).pubsubShardNumSub([regularChannel, shardChannel]); + expect(shardSubscribers).toEqual({ + [regularChannel]: 0, + [shardChannel]: 2, + }); + } finally { + if (client1) { + await clientCleanup(client1, pubSub1 ? pubSub1 : undefined); + } + + if (client2) { + await clientCleanup(client2, pubSub2 ? pubSub2 : undefined); + } + } + }, + TIMEOUT, + ); }); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ea87b86daf..bf840b29d4 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -479,6 +479,12 @@ export async function transactionTest( baseTransaction.publish("test_message", key1); responseData.push(['publish("test_message", key1)', 0]); + baseTransaction.pubsubChannels(); + responseData.push(["pubsubChannels()", []]); + baseTransaction.pubsubNumPat(); + responseData.push(["pubsubNumPat()", 0]); + baseTransaction.pubsubNumSub(); + responseData.push(["pubsubNumSub()", {}]); baseTransaction.flushall(); responseData.push(["flushall()", "OK"]); From 69ac22d51cb4beeb2a0b206717d502b72dda005d Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:56:16 +0300 Subject: [PATCH 143/236] Update CHANGELOG.MD (#2091) Signed-off-by: Shoham Elias --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6730c12791..9c00ed1ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,11 +46,14 @@ * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) -* Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) -* Node: Added XDEL command ([#2064]((https://github.com/valkey-io/valkey-glide/pull/2064)) +* Node: Added XDEL command ([#2064](https://github.com/valkey-io/valkey-glide/pull/2064)) * Node: Added LMPOP & BLMPOP command ([#2050](https://github.com/valkey-io/valkey-glide/pull/2050)) +* Node: Added PUBSUB support ([#1964](https://github.com/valkey-io/valkey-glide/pull/1964)) +* Node: Added PUBSUB * commands ([#2090](https://github.com/valkey-io/valkey-glide/pull/2090)) +* Python: Added PUBSUB * commands ([#2043](https://github.com/valkey-io/valkey-glide/pull/2043)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) From 53bcf18a7ba9a1eac109f0bccc70a0d9540f0017 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Tue, 6 Aug 2024 10:04:34 -0700 Subject: [PATCH 144/236] Python: Fix xclaim documentation (#2075) * Python: Fix xclaim documentation Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 3 ++- python/python/glide/async_commands/core.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c00ed1ffd..dc4b68a9ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,8 @@ #### Fixes * Java: Add overloads for XADD to allow duplicate entry keys ([#1970](https://github.com/valkey-io/valkey-glide/pull/1970)) * Node: Fix ZADD bug where command could not be called with only the `changed` optional parameter ([#1995](https://github.com/valkey-io/valkey-glide/pull/1995)) -* Java: `XRange`/`XRevRange` should return `null` instead of `GlideException` when given a negative count ([#1920](https://github.com/valkey-io/valkey-glide/pull/1920)) +* Java: `XRange`/`XRevRange` should return `null` instead of `GlideException` when given a negative count ([#1920](https://github.com/valkey-io/valkey-glide/pull/1920)) +* Python: Fix `XClaim` return type to `List[bytes]` instead of `List[TEncodable]` ([#2075](https://github.com/valkey-io/valkey-glide/pull/2075)) ## 1.0.0 (2024-07-09) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index a8c0d63062..c61f069a5f 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -3234,7 +3234,7 @@ async def xclaim( options (Optional[StreamClaimOptions]): Stream claim options. Returns: - A Mapping of message entries with the format + Mapping[bytes, List[List[bytes]]]: A Mapping of message entries with the format {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. Examples: @@ -3268,7 +3268,7 @@ async def xclaim_just_id( min_idle_time_ms: int, ids: List[TEncodable], options: Optional[StreamClaimOptions] = None, - ) -> List[TEncodable]: + ) -> List[bytes]: """ Changes the ownership of a pending message. This function returns a List with only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. @@ -3284,7 +3284,7 @@ async def xclaim_just_id( options (Optional[StreamClaimOptions]): Stream claim options. Returns: - A List of message ids claimed by the consumer. + List[bytes]: A List of message ids claimed by the consumer. Examples: # read messages from streamId for consumer1 @@ -3296,7 +3296,7 @@ async def xclaim_just_id( } # "1-0" is now read, and we can assign the pending messages to consumer2 >>> await client.xclaim_just_id("mystream", "mygroup", "consumer2", 0, ["1-0"]) - ["1-0"] + [b"1-0"] """ args = [ @@ -3312,7 +3312,7 @@ async def xclaim_just_id( args.extend(options.to_args()) return cast( - List[TEncodable], + List[bytes], await self._execute_command(RequestType.XClaim, args), ) From 2131fc483d99ee6caa0ffc288faa97cc06f971a1 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Tue, 6 Aug 2024 10:33:24 -0700 Subject: [PATCH 145/236] Node: added GETRANGE command (#2079) * Node: added GETRANGE command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 36 ++++++++++++++++++++++++++++++ node/src/Commands.ts | 15 +++++++++++++ node/src/Transaction.ts | 20 +++++++++++++++++ node/tests/SharedTests.ts | 44 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 118 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4b68a9ca..a3a660a459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * Node: Added TOUCH command ([#2055](https://github.com/valkey-io/valkey-glide/pull/2055)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added GETRANGE command ([#2079](https://github.com/valkey-io/valkey-glide/pull/2079)) * Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) * Node: Added GETBIT command ([#1989](https://github.com/valkey-io/valkey-glide/pull/1989)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0ef0563708..42f259b099 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -73,6 +73,7 @@ import { createGet, createGetBit, createGetDel, + createGetRange, createHDel, createHExists, createHGet, @@ -843,6 +844,41 @@ export class BaseClient { return this.createWritePromise(createGetDel(key)); } + /** + * Returns the substring of the string value stored at `key`, determined by the offsets + * `start` and `end` (both are inclusive). Negative offsets can be used in order to provide + * an offset starting from the end of the string. So `-1` means the last character, `-2` the + * penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` + * or `end` are out of range, returns the substring within the valid range of the string. + * + * See https://valkey.io/commands/getrange/ for details. + * + * @param key - The key of the string. + * @param start - The starting offset. + * @param end - The ending offset. + * @returns A substring extracted from the value stored at `key`. + * + * @example + * ```typescript + * await client.set("mykey", "This is a string") + * let result = await client.getrange("mykey", 0, 3) + * console.log(result); // Output: "This" + * result = await client.getrange("mykey", -3, -1) + * console.log(result); // Output: "ing" - extracted last 3 characters of a string + * result = await client.getrange("mykey", 0, 100) + * console.log(result); // Output: "This is a string" + * result = await client.getrange("mykey", 5, 6) + * console.log(result); // Output: "" + * ``` + */ + public async getrange( + key: string, + start: number, + end: number, + ): Promise { + return this.createWritePromise(createGetRange(key, start, end)); + } + /** 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/Commands.ts b/node/src/Commands.ts index c80e5db0e3..0ecbf1f963 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -104,6 +104,21 @@ export function createGetDel(key: string): command_request.Command { return createCommand(RequestType.GetDel, [key]); } +/** + * @internal + */ +export function createGetRange( + key: string, + start: number, + end: number, +): command_request.Command { + return createCommand(RequestType.GetRange, [ + key, + start.toString(), + end.toString(), + ]); +} + export type SetOptions = { /** * `onlyIfDoesNotExist` - Only set the key if it does not already exist. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 1009b966c1..3a87cd47dd 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -93,6 +93,7 @@ import { createGet, createGetBit, createGetDel, + createGetRange, createHDel, createHExists, createHGet, @@ -287,6 +288,25 @@ export class BaseTransaction> { return this.addAndReturn(createGetDel(key)); } + /** + * Returns the substring of the string value stored at `key`, determined by the offsets + * `start` and `end` (both are inclusive). Negative offsets can be used in order to provide + * an offset starting from the end of the string. So `-1` means the last character, `-2` the + * penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` + * or `end` are out of range, returns the substring within the valid range of the string. + * + * See https://valkey.io/commands/getrange/ for details. + * + * @param key - The key of the string. + * @param start - The starting offset. + * @param end - The ending offset. + * + * Command Response - substring extracted from the value stored at `key`. + */ + public getrange(key: string, start: number, end: number): T { + return this.addAndReturn(createGetRange(key, start, end)); + } + /** 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/tests/SharedTests.ts b/node/tests/SharedTests.ts index dd47041824..bc068c9fc9 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1189,6 +1189,50 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `getrange test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = uuidv4(); + const nonStringKey = uuidv4(); + + expect(await client.set(key, "This is a string")).toEqual("OK"); + expect(await client.getrange(key, 0, 3)).toEqual("This"); + expect(await client.getrange(key, -3, -1)).toEqual("ing"); + expect(await client.getrange(key, 0, -1)).toEqual( + "This is a string", + ); + + // out of range + expect(await client.getrange(key, 10, 100)).toEqual("string"); + expect(await client.getrange(key, -200, -3)).toEqual( + "This is a stri", + ); + expect(await client.getrange(key, 100, 200)).toEqual(""); + + // incorrect range + expect(await client.getrange(key, -1, -3)).toEqual(""); + + // a bug fixed in version 8: https://github.com/redis/redis/issues/13207 + expect(await client.getrange(key, -200, -100)).toEqual( + cluster.checkIfServerVersionLessThan("8.0.0") ? "T" : "", + ); + + // empty key (returning null isn't implemented) + expect(await client.getrange(nonStringKey, 0, -1)).toEqual( + cluster.checkIfServerVersionLessThan("8.0.0") ? "" : null, + ); + + // non-string key + expect(await client.lpush(nonStringKey, ["_"])).toEqual(1); + await expect( + client.getrange(nonStringKey, 0, -1), + ).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 bf840b29d4..75837c65b9 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -500,6 +500,8 @@ export async function transactionTest( responseData.push(['set(key1, "bar")', "OK"]); baseTransaction.randomKey(); responseData.push(["randomKey()", key1]); + baseTransaction.getrange(key1, 0, -1); + responseData.push(["getrange(key1, 0, -1)", "bar"]); baseTransaction.getdel(key1); responseData.push(["getdel(key1)", "bar"]); baseTransaction.set(key1, "bar"); From 607fe31a9e7374f5dc4bc6149f2e8be69e762cf3 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Tue, 6 Aug 2024 15:32:26 -0700 Subject: [PATCH 146/236] Node: Add `XGROUP CREATE` and `XGROUP DESTROY` commands (#2084) * Added XGROUP CREATE and XGROUP DESTROY commands Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 53 ++++++++++++++++++++++ node/src/Commands.ts | 55 +++++++++++++++++++++++ node/src/Transaction.ts | 41 +++++++++++++++++ node/tests/SharedTests.ts | 87 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 13 ++++++ 7 files changed, 252 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a660a459..3403cb5bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ * Node: Added PUBSUB support ([#1964](https://github.com/valkey-io/valkey-glide/pull/1964)) * Node: Added PUBSUB * commands ([#2090](https://github.com/valkey-io/valkey-glide/pull/2090)) * Python: Added PUBSUB * commands ([#2043](https://github.com/valkey-io/valkey-glide/pull/2043)) +* Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 2a0a57eb2b..b92c6dd01e 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -134,6 +134,7 @@ function initialize() { SortClusterOptions, SortOptions, SortedSetRange, + StreamGroupOptions, StreamTrimOptions, StreamAddOptions, StreamReadOptions, @@ -221,6 +222,7 @@ function initialize() { SortClusterOptions, SortOptions, SortedSetRange, + StreamGroupOptions, StreamTrimOptions, StreamAddOptions, StreamReadOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 42f259b099..ffb1c97aa8 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -44,6 +44,7 @@ import { SearchOrigin, SetOptions, StreamAddOptions, + StreamGroupOptions, StreamReadOptions, StreamTrimOptions, ZAddOptions, @@ -152,6 +153,8 @@ import { createWatch, createXAdd, createXDel, + createXGroupCreate, + createXGroupDestroy, createXLen, createXRead, createXTrim, @@ -3669,6 +3672,56 @@ export class BaseClient { return this.createWritePromise(createXLen(key)); } + /** + * Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-create/ for more details. + * + * @param key - The key of the stream. + * @param groupName - The newly created consumer group name. + * @param id - Stream entry ID that specifies the last delivered entry in the stream from the new + * group’s perspective. The special ID `"$"` can be used to specify the last entry in the stream. + * @returns `"OK"`. + * + * @example + * ```typescript + * // Create the consumer group "mygroup", using zero as the starting ID: + * console.log(await client.xgroupCreate("mystream", "mygroup", "0-0")); // Output is "OK" + * ``` + */ + public async xgroupCreate( + key: string, + groupName: string, + id: string, + options?: StreamGroupOptions, + ): Promise { + return this.createWritePromise( + createXGroupCreate(key, groupName, id, options), + ); + } + + /** + * Destroys the consumer group `groupname` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-destroy/ for more details. + * + * @param key - The key of the stream. + * @param groupname - The newly created consumer group name. + * @returns `true` if the consumer group is destroyed. Otherwise, `false`. + * + * @example + * ```typescript + * // Destroys the consumer group "mygroup" + * console.log(await client.xgroupDestroy("mystream", "mygroup")); // Output is true + * ``` + */ + public async xgroupDestroy( + key: string, + groupName: string, + ): Promise { + return this.createWritePromise(createXGroupDestroy(key, groupName)); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0ecbf1f963..fd0402b299 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2312,6 +2312,61 @@ export function createXLen(key: string): command_request.Command { return createCommand(RequestType.XLen, [key]); } +/** + * Optional arguments for {@link BaseClient.xgroupCreate|xgroupCreate}. + * + * See https://valkey.io/commands/xgroup-create/ for more details. + */ +export type StreamGroupOptions = { + /** + * If `true`and the stream doesn't exist, creates a new stream with a length of `0`. + */ + mkStream?: boolean; + /** + * An arbitrary ID (that isn't the first ID, last ID, or the zero `"0-0"`. Use it to + * find out how many entries are between the arbitrary ID (excluding it) and the stream's last + * entry. + * + * since Valkey version 7.0.0. + */ + entriesRead?: string; +}; + +/** + * @internal + */ +export function createXGroupCreate( + key: string, + groupName: string, + id: string, + options?: StreamGroupOptions, +): command_request.Command { + const args: string[] = [key, groupName, id]; + + if (options) { + if (options.mkStream) { + args.push("MKSTREAM"); + } + + if (options.entriesRead) { + args.push("ENTRIESREAD"); + args.push(options.entriesRead); + } + } + + return createCommand(RequestType.XGroupCreate, args); +} + +/** + * @internal + */ +export function createXGroupDestroy( + key: string, + groupName: string, +): command_request.Command { + return createCommand(RequestType.XGroupDestroy, [key, groupName]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3a87cd47dd..2795f85d2e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -48,6 +48,7 @@ import { SortClusterOptions, SortOptions, StreamAddOptions, + StreamGroupOptions, StreamReadOptions, StreamTrimOptions, ZAddOptions, @@ -186,6 +187,8 @@ import { createXLen, createXRead, createXTrim, + createXGroupCreate, + createXGroupDestroy, createZAdd, createZCard, createZCount, @@ -2125,6 +2128,44 @@ export class BaseTransaction> { return this.addAndReturn(createXLen(key)); } + /** + * Creates a new consumer group uniquely identified by `groupname` for the stream + * stored at `key`. + * + * See https://valkey.io/commands/xgroup-create/ for more details. + * + * @param key - The key of the stream. + * @param groupName - The newly created consumer group name. + * @param id - Stream entry ID that specifies the last delivered entry in the stream from the new + * group’s perspective. The special ID `"$"` can be used to specify the last entry in the stream. + * + * Command Response - `"OK"`. + */ + public xgroupCreate( + key: string, + groupName: string, + id: string, + options?: StreamGroupOptions, + ): T { + return this.addAndReturn( + createXGroupCreate(key, groupName, id, options), + ); + } + + /** + * Destroys the consumer group `groupname` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-destroy/ for more details. + * + * @param key - The key of the stream. + * @param groupname - The newly created consumer group name. + * + * Command Response - `true` if the consumer group is destroyed. Otherwise, `false`. + */ + public xgroupDestroy(key: string, groupName: string): T { + return this.addAndReturn(createXGroupDestroy(key, groupName)); + } + /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index bc068c9fc9..b045b866c4 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6776,6 +6776,93 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xgroupCreate and xgroupDestroy test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = uuidv4(); + const nonExistentKey = uuidv4(); + const stringKey = uuidv4(); + const groupName1 = uuidv4(); + const groupName2 = uuidv4(); + const streamId = "0-1"; + + // trying to create a consumer group for a non-existing stream without the "MKSTREAM" arg results in error + await expect( + client.xgroupCreate(nonExistentKey, groupName1, streamId), + ).rejects.toThrow(RequestError); + + // calling with the "MKSTREAM" arg should create the new stream automatically + expect( + await client.xgroupCreate(key, groupName1, streamId, { + mkStream: true, + }), + ).toEqual("OK"); + + // invalid arg - group names must be unique, but group_name1 already exists + await expect( + client.xgroupCreate(key, groupName1, streamId), + ).rejects.toThrow(RequestError); + + // Invalid stream ID format + await expect( + client.xgroupCreate( + key, + groupName2, + "invalid_stream_id_format", + ), + ).rejects.toThrow(RequestError); + + expect(await client.xgroupDestroy(key, groupName1)).toEqual( + true, + ); + // calling xgroup_destroy again returns False because the group was already destroyed above + expect(await client.xgroupDestroy(key, groupName1)).toEqual( + false, + ); + + // attempting to destroy a group for a non-existing key should raise an error + await expect( + client.xgroupDestroy(nonExistentKey, groupName1), + ).rejects.toThrow(RequestError); + + // "ENTRIESREAD" option was added in Valkey 7.0.0 + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + await expect( + client.xgroupCreate(key, groupName1, streamId, { + entriesRead: "10", + }), + ).rejects.toThrow(RequestError); + } else { + expect( + await client.xgroupCreate(key, groupName1, streamId, { + entriesRead: "10", + }), + ).toEqual("OK"); + + // invalid entries_read_id - cannot be the zero ("0-0") ID + await expect( + client.xgroupCreate(key, groupName1, streamId, { + entriesRead: "0-0", + }), + ).rejects.toThrow(RequestError); + } + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xgroupCreate(stringKey, groupName1, streamId, { + mkStream: true, + }), + ).rejects.toThrow(RequestError); + await expect( + client.xgroupDestroy(stringKey, groupName1), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 75837c65b9..c1983f38cf 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -474,6 +474,8 @@ export async function transactionTest( const key24 = "{key}" + uuidv4(); // list value const field = uuidv4(); const value = uuidv4(); + const groupName1 = uuidv4(); + const groupName2 = uuidv4(); // array of tuples - first element is test name/description, second - expected return value const responseData: [string, ReturnType][] = []; @@ -877,6 +879,17 @@ export async function transactionTest( 'xtrim(key9, { method: "minid", threshold: "0-2", exact: true }', 1, ]); + baseTransaction.xgroupCreate(key9, groupName1, "0-0"); + responseData.push(['xgroupCreate(key9, groupName1, "0-0")', "OK"]); + baseTransaction.xgroupCreate(key9, groupName2, "0-0", { mkStream: true }); + responseData.push([ + 'xgroupCreate(key9, groupName2, "0-0", { mkStream: true })', + "OK", + ]); + baseTransaction.xgroupDestroy(key9, groupName1); + responseData.push(["xgroupDestroy(key9, groupName1)", true]); + baseTransaction.xgroupDestroy(key9, groupName2); + responseData.push(["xgroupDestroy(key9, groupName2)", true]); baseTransaction.xdel(key9, ["0-3", "0-5"]); responseData.push(["xdel(key9, [['0-3', '0-5']])", 1]); baseTransaction.rename(key9, key10); From 45deec51cd8bab8903347812fd1547b2d2b52bfc Mon Sep 17 00:00:00 2001 From: ort-bot Date: Wed, 7 Aug 2024 00:21:45 +0000 Subject: [PATCH 147/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 2 +- java/THIRD_PARTY_LICENSES_JAVA | 2 +- node/THIRD_PARTY_LICENSES_NODE | 2 +- python/THIRD_PARTY_LICENSES_PYTHON | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index b40256be0f..3508f83266 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -20267,7 +20267,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.7.0 +Package: rustls-pki-types:1.8.0 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 18554d333a..bd1e88cb68 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -21162,7 +21162,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.7.0 +Package: rustls-pki-types:1.8.0 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 e035208e1f..88edc5a8c0 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -21611,7 +21611,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.7.0 +Package: rustls-pki-types:1.8.0 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 a430e5c146..97ecdd3b01 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -22332,7 +22332,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.7.0 +Package: rustls-pki-types:1.8.0 The following copyrights and licenses were found in the source code of this package: @@ -34880,7 +34880,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: attrs:24.1.0 +Package: attrs:24.2.0 The following copyrights and licenses were found in the source code of this package: From 834a932e7fb2f8f6ac125102d22da5dd50a4c484 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 08:51:33 -0700 Subject: [PATCH 148/236] Node: Add `GEOSEARCHSTORE` command. (#2080) * Add `GEOSEARCHSTORE` command. Signed-off-by: Yury-Fridlyand * Signed-off-by: Yury-Fridlyand --------- Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + .../GeospatialIndicesBaseCommands.java | 16 +- .../geospatial/GeoSearchStoreOptions.java | 9 +- node/src/BaseClient.ts | 92 ++++++- node/src/Commands.ts | 62 ++++- node/src/Transaction.ts | 44 +++ node/tests/GlideClusterClient.test.ts | 8 +- node/tests/SharedTests.ts | 250 ++++++++++++++++-- node/tests/TestUtilities.ts | 12 + 9 files changed, 445 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3403cb5bc9..a149637d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) * Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) +* Node: Added GEOSEARCHSTORE command ([#2080](https://github.com/valkey-io/valkey-glide/pull/2080)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) diff --git a/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java index 7abb252747..86ae648faa 100644 --- a/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java @@ -768,7 +768,7 @@ CompletableFuture geosearch( * axis-aligned rectangle, determined by height and width. * * - * @return The number of elements in the resulting set. + * @return The number of elements in the resulting sorted set stored at destination. * @example *

{@code
      * Long result = client
@@ -812,7 +812,7 @@ CompletableFuture geosearchstore(
      *           axis-aligned rectangle, determined by height and width.
      *     
      *
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -861,7 +861,7 @@ CompletableFuture geosearchstore(
      *
      * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
      *     GeoSearchResultOptions}
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -912,7 +912,7 @@ CompletableFuture geosearchstore(
      *
      * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
      *     GeoSearchResultOptions}
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -962,7 +962,7 @@ CompletableFuture geosearchstore(
      *     
      *
      * @param options The optional inputs to request additional information.
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1012,7 +1012,7 @@ CompletableFuture geosearchstore(
      *     
      *
      * @param options The optional inputs to request additional information.
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1064,7 +1064,7 @@ CompletableFuture geosearchstore(
      * @param options The optional inputs to request additional information.
      * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
      *     GeoSearchResultOptions}
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1118,7 +1118,7 @@ CompletableFuture geosearchstore(
      * @param options The optional inputs to request additional information.
      * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
      *     GeoSearchResultOptions}
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
index 047273aaa3..384567b56d 100644
--- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
+++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
@@ -15,7 +15,14 @@ public final class GeoSearchStoreOptions {
     /** Valkey API keyword for {@link #storeDist} parameter. */
     public static final String GEOSEARCHSTORE_VALKEY_API = "STOREDIST";
 
-    /** Configure sorting the results with their distance from the center. */
+    /**
+     * Determines what is stored as the sorted set score. Defaults to false.
+ * If set to false, the geohash of the location will be stored as the sorted set + * score.
+ * If set to true, the distance from the center of the shape (circle or box) will be + * stored as the sorted set score. The distance is represented as a floating-point number in the + * same unit specified for that shape. + */ private final boolean storeDist; /** diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ffb1c97aa8..1b3d85be63 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -29,6 +29,7 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, + GeoSearchStoreResultOptions, GeoUnit, GeospatialData, InsertPosition, @@ -71,6 +72,7 @@ import { createGeoHash, createGeoPos, createGeoSearch, + createGeoSearchStore, createGet, createGetBit, createGetDel, @@ -4202,22 +4204,15 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param searchFrom - The query's center point options, could be one of: - * * - {@link MemberOrigin} to use the position of the given existing member in the sorted set. - * * - {@link CoordOrigin} to use the given longitude and latitude coordinates. - * * @param searchBy - The query's shape options, could be one of: - * * - {@link GeoCircleShape} to search inside circular area according to given radius. - * * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. - * - * @param resultOptions - The optional inputs to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}. + * @param resultOptions - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}. * @returns By default, returns an `Array` of members (locations) names. * If any of `withCoord`, `withDist` or `withHash` are set to `true` in {@link GeoSearchResultOptions}, a 2D `Array` returned, * where each sub-array represents a single item in the following order: - * * - The member (location) name. * - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`. * - The geohash of the location as a integer `number`, if `withHash` is set to `true`. @@ -4271,12 +4266,91 @@ export class BaseClient { searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchResultOptions, - ): Promise<(Buffer | (number | number[])[])[]> { + ): Promise<(string | (number | number[])[])[]> { return this.createWritePromise( createGeoSearch(key, searchFrom, searchBy, resultOptions), ); } + /** + * Searches for members in a sorted set stored at `source` representing geospatial data + * within a circular or rectangular area and stores the result in `destination`. + * + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * To get the result directly, see {@link geosearch}. + * + * See https://valkey.io/commands/geosearchstore/ for more details. + * + * since - Valkey 6.2.0 and above. + * + * @remarks When in cluster mode, `destination` and `source` must map to the same hash slot. + * + * @param destination - The key of the destination sorted set. + * @param source - The key of the sorted set. + * @param searchFrom - The query's center point options, could be one of: + * - {@link MemberOrigin} to use the position of the given existing member in the sorted set. + * - {@link CoordOrigin} to use the given longitude and latitude coordinates. + * @param searchBy - The query's shape options, could be one of: + * - {@link GeoCircleShape} to search inside circular area according to given radius. + * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. + * @param resultOptions - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchStoreResultOptions}. + * @returns The number of elements in the resulting sorted set stored at `destination`. + * + * @example + * ```typescript + * const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]); + * await client.geoadd("mySortedSet", data); + * // search for locations within 200 km circle around stored member named 'Palermo' and store in `destination`: + * await client.geosearchstore("destination", "mySortedSet", { member: "Palermo" }, { radius: 200, unit: GeoUnit.KILOMETERS }); + * // query the stored results + * const result1 = await client.zrangeWithScores("destination", { start: 0, stop: -1 }); + * console.log(result1); // Output: + * // { + * // Palermo: 3479099956230698, // geohash of the location is stored as element's score + * // Catania: 3479447370796909 + * // } + * + * // search for locations in 200x300 mi rectangle centered at coordinate (15, 37), requesting to store distance instead of geohashes, + * // limiting results by 2 best matches, ordered by ascending distance from the search area center + * await client.geosearchstore( + * "destination", + * "mySortedSet", + * { position: { longitude: 15, latitude: 37 } }, + * { width: 200, height: 300, unit: GeoUnit.MILES }, + * { + * sortOrder: SortOrder.ASC, + * count: 2, + * storeDist: true, + * }, + * ); + * // query the stored results + * const result2 = await client.zrangeWithScores("destination", { start: 0, stop: -1 }); + * console.log(result2); // Output: + * // { + * // Palermo: 190.4424, // distance from the search area center is stored as element's score + * // Catania: 56.4413, // the distance is measured in units used for the search query (miles) + * // } + * ``` + */ + public async geosearchstore( + destination: string, + source: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchStoreResultOptions, + ): Promise { + return this.createWritePromise( + createGeoSearchStore( + destination, + source, + searchFrom, + searchBy, + resultOptions, + ), + ); + } + /** * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index fd0402b299..f807d8234e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2699,7 +2699,7 @@ export function createGeoHash( * Optional parameters for {@link BaseClient.geosearch|geosearch} command which defines what should be included in the * search results and how results should be ordered and limited. */ -export type GeoSearchResultOptions = { +export type GeoSearchResultOptions = GeoSearchCommonResultOptions & { /** Include the coordinate of the returned items. */ withCoord?: boolean; /** @@ -2709,6 +2709,22 @@ export type GeoSearchResultOptions = { withDist?: boolean; /** Include the geohash of the returned items. */ withHash?: boolean; +}; + +/** + * Optional parameters for {@link BaseClient.geosearchstore|geosearchstore} command which defines what should be included in the + * search results and how results should be ordered and limited. + */ +export type GeoSearchStoreResultOptions = GeoSearchCommonResultOptions & { + /** + * Determines what is stored as the sorted set score. Defaults to `false`. + * - If set to `false`, the geohash of the location will be stored as the sorted set score. + * - If set to `true`, the distance from the center of the shape (circle or box) will be stored as the sorted set score. The distance is represented as a floating-point number in the same unit specified for that shape. + */ + storeDist?: boolean; +}; + +type GeoSearchCommonResultOptions = { /** Indicates the order the result should be sorted in. */ sortOrder?: SortOrder; /** Indicates the number of matches the result should be limited to. */ @@ -2759,16 +2775,39 @@ export type MemberOrigin = { member: string; }; -/** - * @internal - */ +/** @internal */ export function createGeoSearch( key: string, searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchResultOptions, ): command_request.Command { - let args: string[] = [key]; + const args = [key].concat( + convertGeoSearchOptionsToArgs(searchFrom, searchBy, resultOptions), + ); + return createCommand(RequestType.GeoSearch, args); +} + +/** @internal */ +export function createGeoSearchStore( + destination: string, + source: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchStoreResultOptions, +): command_request.Command { + const args = [destination, source].concat( + convertGeoSearchOptionsToArgs(searchFrom, searchBy, resultOptions), + ); + return createCommand(RequestType.GeoSearchStore, args); +} + +function convertGeoSearchOptionsToArgs( + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchCommonResultOptions, +): string[] { + let args: string[] = []; if ("position" in searchFrom) { args = args.concat( @@ -2796,9 +2835,14 @@ export function createGeoSearch( } if (resultOptions) { - if (resultOptions.withCoord) args.push("WITHCOORD"); - if (resultOptions.withDist) args.push("WITHDIST"); - if (resultOptions.withHash) args.push("WITHHASH"); + if ("withCoord" in resultOptions && resultOptions.withCoord) + args.push("WITHCOORD"); + if ("withDist" in resultOptions && resultOptions.withDist) + args.push("WITHDIST"); + if ("withHash" in resultOptions && resultOptions.withHash) + args.push("WITHHASH"); + if ("storeDist" in resultOptions && resultOptions.storeDist) + args.push("STOREDIST"); if (resultOptions.count) { args.push("COUNT", resultOptions.count?.toString()); @@ -2809,7 +2853,7 @@ export function createGeoSearch( if (resultOptions.sortOrder) args.push(resultOptions.sortOrder); } - return createCommand(RequestType.GeoSearch, args); + return args; } /** diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2795f85d2e..e2babf630f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -215,6 +215,8 @@ import { createZRevRankWithScore, createZScan, createZScore, + createGeoSearchStore, + GeoSearchStoreResultOptions, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2575,6 +2577,48 @@ export class BaseTransaction> { ); } + /** + * Searches for members in a sorted set stored at `source` representing geospatial data + * within a circular or rectangular area and stores the result in `destination`. + * + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * To get the result directly, see {@link geosearch}. + * + * See https://valkey.io/commands/geosearchstore/ for more details. + * + * since - Valkey 6.2.0 and above. + * + * @param destination - The key of the destination sorted set. + * @param source - The key of the sorted set. + * @param searchFrom - The query's center point options, could be one of: + * - {@link MemberOrigin} to use the position of the given existing member in the sorted set. + * - {@link CoordOrigin} to use the given longitude and latitude coordinates. + * @param searchBy - The query's shape options, could be one of: + * - {@link GeoCircleShape} to search inside circular area according to given radius. + * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. + * @param resultOptions - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchStoreResultOptions}. + * + * Command Response - The number of elements in the resulting sorted set stored at `destination`. + */ + public geosearchstore( + destination: string, + source: string, + searchFrom: SearchOrigin, + searchBy: GeoSearchShape, + resultOptions?: GeoSearchStoreResultOptions, + ): T { + return this.addAndReturn( + createGeoSearchStore( + destination, + source, + searchFrom, + searchBy, + resultOptions, + ), + ); + } + /** * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 930df79c62..1edb82e84b 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -25,7 +25,7 @@ import { ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; +import { FlushMode, GeoUnit, SortOrder } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -354,6 +354,12 @@ describe("GlideClusterClient", () => { client.zdiffWithScores(["abc", "zxy", "lkn"]), client.zdiffstore("abc", ["zxy", "lkn"]), client.copy("abc", "zxy", true), + client.geosearchstore( + "abc", + "zxy", + { member: "_" }, + { radius: 5, unit: GeoUnit.METERS }, + ), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b045b866c4..1ef69cc727 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5603,12 +5603,14 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `geosearch test_%p`, + `geosearch geosearchstore test_%p`, async (protocol) => { await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("6.2.0")) return; - const key = uuidv4(); + const key1 = "{geosearch}" + uuidv4(); + const key2 = "{geosearch}" + uuidv4(); + const key3 = "{geosearch}" + uuidv4(); const members: string[] = [ "Catania", @@ -5672,30 +5674,55 @@ export function runBaseTests(config: { ]; // geoadd - expect(await client.geoadd(key, membersToCoordinates)).toBe( + expect(await client.geoadd(key1, membersToCoordinates)).toBe( members.length, ); let searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, ); // using set to compare, because results are reordrered expect(new Set(searchResult)).toEqual(membersSet); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // order search result searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual(members); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { sortOrder: SortOrder.ASC, storeDist: true }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // order and query all extra data searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { @@ -5709,7 +5736,7 @@ export function runBaseTests(config: { // order, query and limit by 1 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { @@ -5721,11 +5748,28 @@ export function runBaseTests(config: { }, ); expect(searchResult).toEqual(expectedResult.slice(0, 1)); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 1, + storeDist: true, + }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual([members[0]]); // test search by box, unit: meters, from member, with distance const meters = 400 * 1000; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { width: meters, height: meters, unit: GeoUnit.METERS }, { @@ -5739,11 +5783,33 @@ export function runBaseTests(config: { ["Palermo", [166274.1516]], ["Catania", [0.0]], ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { width: meters, height: meters, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(3); + // TODO deep close to https://github.com/maasencioh/jest-matcher-deep-close-to + expect( + await client.zrangeWithScores( + key2, + { start: 0, stop: -1 }, + true, + ), + ).toEqual({ + edge2: 236529.17986494553, + Palermo: 166274.15156960033, + Catania: 0.0, + }); // test search by box, unit: feet, from member, with limited count 2, with hash const feet = 400 * 3280.8399; searchResult = await client.geosearch( - key, + key1, { member: "Palermo" }, { width: feet, height: feet, unit: GeoUnit.FEET }, { @@ -5758,39 +5824,97 @@ export function runBaseTests(config: { ["Palermo", [3479099956230698]], ["edge1", [3479273021651468]], ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Palermo" }, + { width: feet, height: feet, unit: GeoUnit.FEET }, + { + sortOrder: SortOrder.ASC, + count: 2, + }, + ), + ).toEqual(2); + expect( + await client.zrangeWithScores(key2, { start: 0, stop: -1 }), + ).toEqual({ + Palermo: 3479099956230698, + edge1: 3479273021651468, + }); // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 const miles = 250; searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: miles, height: miles, unit: GeoUnit.MILES }, { count: 1, isAny: true }, ); expect(members).toContainEqual(searchResult[0]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: miles, height: miles, unit: GeoUnit.MILES }, + { count: 1, isAny: true }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // test search by radius, units: feet, from member const feetRadius = 200 * 3280.8399; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: feetRadius, unit: GeoUnit.FEET }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual(["Catania", "Palermo"]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { radius: feetRadius, unit: GeoUnit.FEET }, + { sortOrder: SortOrder.ASC, storeDist: true }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // Test search by radius, unit: meters, from member const metersRadius = 200 * 1000; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: metersRadius, unit: GeoUnit.METERS }, { sortOrder: SortOrder.DESC }, ); expect(searchResult).toEqual(["Palermo", "Catania"]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { radius: metersRadius, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }, true), + ).toEqual(searchResult); searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: metersRadius, unit: GeoUnit.METERS }, { @@ -5805,7 +5929,7 @@ export function runBaseTests(config: { // Test search by radius, unit: miles, from geospatial data searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 175, unit: GeoUnit.MILES }, { sortOrder: SortOrder.DESC }, @@ -5816,10 +5940,23 @@ export function runBaseTests(config: { "Palermo", "Catania", ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 175, unit: GeoUnit.MILES }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }, true), + ).toEqual(searchResult); // Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 200, unit: GeoUnit.KILOMETERS }, { @@ -5831,10 +5968,27 @@ export function runBaseTests(config: { }, ); expect(searchResult).toEqual(expectedResult.slice(0, 2)); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 2, + storeDist: true, + }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(members.slice(0, 2)); // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 200, unit: GeoUnit.KILOMETERS }, { @@ -5847,40 +6001,94 @@ export function runBaseTests(config: { }, ); expect(members).toContainEqual(searchResult[0][0]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 1, + isAny: true, + }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual([searchResult[0][0]]); // no members within the area searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 50, height: 50, unit: GeoUnit.METERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual([]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 50, height: 50, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ), + ).toEqual(0); + expect(await client.zcard(key2)).toEqual(0); // no members within the area searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 5, unit: GeoUnit.METERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual([]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 5, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ), + ).toEqual(0); + expect(await client.zcard(key2)).toEqual(0); // member does not exist await expect( client.geosearch( - key, + key1, + { member: "non-existing-member" }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + await expect( + client.geosearchstore( + key2, + key1, { member: "non-existing-member" }, { radius: 100, unit: GeoUnit.METERS }, ), ).rejects.toThrow(RequestError); // key exists but holds a non-ZSET value - const key2 = uuidv4(); - expect(await client.set(key2, uuidv4())).toEqual("OK"); + expect(await client.set(key3, uuidv4())).toEqual("OK"); await expect( client.geosearch( + key3, + { position: { longitude: 15, latitude: 37 } }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + await expect( + client.geosearchstore( key2, + key3, { position: { longitude: 15, latitude: 37 } }, { radius: 100, unit: GeoUnit.METERS }, ), diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index c1983f38cf..f8def33988 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -472,6 +472,7 @@ export async function transactionTest( const key22 = "{key}" + uuidv4(); // list for sort const key23 = "{key}" + uuidv4(); // zset random const key24 = "{key}" + uuidv4(); // list value + const key25 = "{key}" + uuidv4(); // Geospatial Data/ZSET const field = uuidv4(); const value = uuidv4(); const groupName1 = uuidv4(); @@ -1104,6 +1105,17 @@ export async function transactionTest( ], ], ]); + + baseTransaction.geosearchstore( + key25, + key18, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ); + responseData.push([ + "geosearchstore(key25, key18, (15, 37), 400x400 KM)", + 2, + ]); } const libName = "mylib1C" + uuidv4().replaceAll("-", ""); From 4e284049b45ddfb1528f2b80b7c0b5f9f0527582 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 09:13:08 -0700 Subject: [PATCH 149/236] Node: Add `XCLAIM` command. (#2092) * Add `XCLAIM` command. --------- Signed-off-by: Yury-Fridlyand Co-authored-by: Guian Gumpac --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 94 +++++++++++++++++++++++++--- node/src/Commands.ts | 63 +++++++++++++++++++ node/src/Transaction.ts | 57 +++++++++++++++++ node/tests/SharedTests.ts | 120 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 60 +++++++++++++++++- 7 files changed, 385 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a149637d19..7575a797c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XCLAIM command ([#2092](https://github.com/valkey-io/valkey-glide/pull/2092)) * Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) * Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index b92c6dd01e..ef1b324437 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -138,6 +138,7 @@ function initialize() { StreamTrimOptions, StreamAddOptions, StreamReadOptions, + StreamClaimOptions, ScriptOptions, ClosingError, ConfigurationError, @@ -226,6 +227,7 @@ function initialize() { StreamTrimOptions, StreamAddOptions, StreamReadOptions, + StreamClaimOptions, ScriptOptions, ClosingError, ConfigurationError, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1b3d85be63..5dcf0fed8b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -186,6 +186,8 @@ import { createZRevRankWithScore, createZScan, createZScore, + StreamClaimOptions, + createXClaim, } from "./Commands"; import { ClosingError, @@ -3638,21 +3640,23 @@ export class BaseClient { * @example * ```typescript * const streamResults = await client.xread({"my_stream": "0-0", "writers": "0-0"}); - * console.log(result); // Output: { - * // "my_stream": { - * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], - * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], - * // }, "writers": { - * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], - * // "1526985685298-0": [["name", "Jane"], ["surname", "Austen"]], - * // } - * // } + * console.log(result); // Output: + * // { + * // "my_stream": { + * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], + * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // }, + * // "writers": { + * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], + * // "1526985685298-0": [["name", "Jane"], ["surname", "Austen"]], + * // } + * // } * ``` */ public xread( keys_and_ids: Record, options?: StreamReadOptions, - ): Promise>> { + ): Promise>> { return this.createWritePromise(createXRead(keys_and_ids, options)); } @@ -3674,6 +3678,76 @@ export class BaseClient { return this.createWritePromise(createXLen(key)); } + /** + * Changes the ownership of a pending message. + * + * See https://valkey.io/commands/xclaim/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param ids - An array of entry ids. + * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. + * @returns A `Record` of message entries that are claimed by the consumer. + * + * @example + * ```typescript + * const result = await client.xclaim("myStream", "myGroup", "myConsumer", 42, + * ["1-0", "2-0", "3-0"], { idle: 500, retryCount: 3, isForce: true }); + * console.log(result); // Output: + * // { + * // "2-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]] + * // } + * ``` + */ + public async xclaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + ids: string[], + options?: StreamClaimOptions, + ): Promise> { + return this.createWritePromise( + createXClaim(key, group, consumer, minIdleTime, ids, options), + ); + } + + /** + * Changes the ownership of a pending message. This function returns an `array` with + * only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. + * + * See https://valkey.io/commands/xclaim/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param ids - An array of entry ids. + * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. + * @returns An `array` of message ids claimed by the consumer. + * + * @example + * ```typescript + * const result = await client.xclaimJustId("my_stream", "my_group", "my_consumer", 42, + * ["1-0", "2-0", "3-0"], { idle: 500, retryCount: 3, isForce: true }); + * console.log(result); // Output: [ "2-0", "3-0" ] + * ``` + */ + public async xclaimJustId( + key: string, + group: string, + consumer: string, + minIdleTime: number, + ids: string[], + options?: StreamClaimOptions, + ): Promise { + return this.createWritePromise( + createXClaim(key, group, consumer, minIdleTime, ids, options, true), + ); + } + /** * Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f807d8234e..8353f79c29 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2312,6 +2312,69 @@ export function createXLen(key: string): command_request.Command { return createCommand(RequestType.XLen, [key]); } +/** Optional parameters for {@link BaseClient.xclaim|xclaim} command. */ +export type StreamClaimOptions = { + /** + * Set the idle time (last time it was delivered) of the message in milliseconds. If `idle` + * is not specified, an `idle` of `0` is assumed, that is, the time count is reset + * because the message now has a new owner trying to process it. + */ + idle?: number; // in milliseconds + + /** + * This is the same as {@link idle} but instead of a relative amount of milliseconds, it sets the + * idle time to a specific Unix time (in milliseconds). This is useful in order to rewrite the AOF + * file generating `XCLAIM` commands. + */ + idleUnixTime?: number; // in unix-time milliseconds + + /** + * Set the retry counter to the specified value. This counter is incremented every time a message + * is delivered again. Normally {@link BaseClient.xclaim|xclaim} does not alter this counter, + * which is just served to clients when the {@link BaseClient.xpending|xpending} command is called: + * this way clients can detect anomalies, like messages that are never processed for some reason + * after a big number of delivery attempts. + */ + retryCount?: number; + + /** + * Creates the pending message entry in the PEL even if certain specified IDs are not already in + * the PEL assigned to a different client. However, the message must exist in the stream, + * otherwise the IDs of non-existing messages are ignored. + */ + isForce?: boolean; + + /** The last ID of the entry which should be claimed. */ + lastId?: string; +}; + +/** @internal */ +export function createXClaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + ids: string[], + options?: StreamClaimOptions, + justId?: boolean, +): command_request.Command { + const args = [key, group, consumer, minIdleTime.toString(), ...ids]; + + if (options) { + if (options.idle !== undefined) + args.push("IDLE", options.idle.toString()); + if (options.idleUnixTime !== undefined) + args.push("TIME", options.idleUnixTime.toString()); + if (options.retryCount !== undefined) + args.push("RETRYCOUNT", options.retryCount.toString()); + if (options.isForce) args.push("FORCE"); + if (options.lastId) args.push("LASTID", options.lastId); + } + + if (justId) args.push("JUSTID"); + return createCommand(RequestType.XClaim, args); +} + /** * Optional arguments for {@link BaseClient.xgroupCreate|xgroupCreate}. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e2babf630f..20e35b49ee 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -48,6 +48,7 @@ import { SortClusterOptions, SortOptions, StreamAddOptions, + StreamClaimOptions, StreamGroupOptions, StreamReadOptions, StreamTrimOptions, @@ -183,6 +184,7 @@ import { createType, createUnlink, createXAdd, + createXClaim, createXDel, createXLen, createXRead, @@ -2130,6 +2132,61 @@ export class BaseTransaction> { return this.addAndReturn(createXLen(key)); } + /** + * Changes the ownership of a pending message. + * + * See https://valkey.io/commands/xclaim/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param ids - An array of entry ids. + * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. + * + * Command Response - A `Record` of message entries that are claimed by the consumer. + */ + public xclaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + ids: string[], + options?: StreamClaimOptions, + ): T { + return this.addAndReturn( + createXClaim(key, group, consumer, minIdleTime, ids, options), + ); + } + + /** + * Changes the ownership of a pending message. This function returns an `array` with + * only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. + * + * See https://valkey.io/commands/xclaim/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param ids - An array of entry ids. + * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. + * + * Command Response - An `array` of message ids claimed by the consumer. + */ + public xclaimJustId( + key: string, + group: string, + consumer: string, + minIdleTime: number, + ids: string[], + options?: StreamClaimOptions, + ): T { + return this.addAndReturn( + createXClaim(key, group, consumer, minIdleTime, ids, options, true), + ); + } + /** * Creates a new consumer group uniquely identified by `groupname` for the stream * stored at `key`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1ef69cc727..f41cd4a968 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6873,6 +6873,126 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xclaim test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const group = uuidv4(); + + expect( + await client.xgroupCreate(key, group, "0", { + mkStream: true, + }), + ).toEqual("OK"); + expect( + await client.customCommand([ + "xgroup", + "createconsumer", + key, + group, + "consumer", + ]), + ).toEqual(true); + + expect( + await client.xadd( + key, + [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + { id: "0-1" }, + ), + ).toEqual("0-1"); + expect( + await client.xadd( + key, + [["entry2_field1", "entry2_value1"]], + { id: "0-2" }, + ), + ).toEqual("0-2"); + + expect( + await client.customCommand([ + "xreadgroup", + "group", + group, + "consumer", + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + "0-1": [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + "0-2": [["entry2_field1", "entry2_value1"]], + }, + }); + + expect( + await client.xclaim(key, group, "consumer", 0, ["0-1"]), + ).toEqual({ + "0-1": [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + }); + expect( + await client.xclaimJustId(key, group, "consumer", 0, [ + "0-2", + ]), + ).toEqual(["0-2"]); + + // add one more entry + expect( + await client.xadd( + key, + [["entry3_field1", "entry3_value1"]], + { id: "0-3" }, + ), + ).toEqual("0-3"); + // using force, we can xclaim the message without reading it + expect( + await client.xclaimJustId( + key, + group, + "consumer", + 0, + ["0-3"], + { isForce: true, retryCount: 99 }, + ), + ).toEqual(["0-3"]); + + // incorrect IDs - response is empty + expect( + await client.xclaim(key, group, "consumer", 0, ["000"]), + ).toEqual({}); + expect( + await client.xclaimJustId(key, group, "consumer", 0, [ + "000", + ]), + ).toEqual([]); + + // empty ID array + await expect( + client.xclaim(key, group, "consumer", 0, []), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a stream + const stringKey = uuidv4(); + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xclaim(stringKey, "_", "_", 0, ["_"]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lmpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index f8def33988..22283738f7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -887,12 +887,68 @@ export async function transactionTest( 'xgroupCreate(key9, groupName2, "0-0", { mkStream: true })', "OK", ]); + baseTransaction.xdel(key9, ["0-3", "0-5"]); + responseData.push(["xdel(key9, [['0-3', '0-5']])", 1]); + + // key9 has one entry here: {"0-2":[["field","value2"]]} + + baseTransaction.customCommand([ + "xgroup", + "createconsumer", + key9, + groupName1, + "consumer1", + ]); + responseData.push([ + 'xgroupCreateConsumer(key9, groupName1, "consumer1")', + true, + ]); + baseTransaction.customCommand([ + "xreadgroup", + "group", + groupName1, + "consumer1", + "STREAMS", + key9, + ">", + ]); + responseData.push([ + 'xreadgroup(groupName1, "consumer1", key9, >)', + { [key9]: { "0-2": [["field", "value2"]] } }, + ]); + baseTransaction.xclaim(key9, groupName1, "consumer1", 0, ["0-2"]); + responseData.push([ + 'xclaim(key9, groupName1, "consumer1", 0, ["0-2"])', + { "0-2": [["field", "value2"]] }, + ]); + baseTransaction.xclaim(key9, groupName1, "consumer1", 0, ["0-2"], { + isForce: true, + retryCount: 0, + idle: 0, + }); + responseData.push([ + 'xclaim(key9, groupName1, "consumer1", 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', + { "0-2": [["field", "value2"]] }, + ]); + baseTransaction.xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"]); + responseData.push([ + 'xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"])', + ["0-2"], + ]); + baseTransaction.xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"], { + isForce: true, + retryCount: 0, + idle: 0, + }); + responseData.push([ + 'xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', + ["0-2"], + ]); baseTransaction.xgroupDestroy(key9, groupName1); responseData.push(["xgroupDestroy(key9, groupName1)", true]); baseTransaction.xgroupDestroy(key9, groupName2); responseData.push(["xgroupDestroy(key9, groupName2)", true]); - baseTransaction.xdel(key9, ["0-3", "0-5"]); - responseData.push(["xdel(key9, [['0-3', '0-5']])", 1]); + baseTransaction.rename(key9, key10); responseData.push(["rename(key9, key10)", "OK"]); baseTransaction.exists([key10]); From 80f8153479c91980428f0a5f00047f608cf88bd3 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 09:31:45 -0700 Subject: [PATCH 150/236] Node: Add `FUNCTION STATS` command. (#2082) * Add `FUNCTION STATS` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/Commands.ts | 13 ++++++ node/src/GlideClient.ts | 54 +++++++++++++++++++++++ node/src/GlideClusterClient.ts | 63 ++++++++++++++++++++++++++- node/src/Transaction.ts | 19 ++++++++ node/tests/GlideClient.test.ts | 11 ++++- node/tests/GlideClusterClient.test.ts | 51 +++++++++++++++++++++- node/tests/TestUtilities.ts | 48 ++++++++++++++++++++ 9 files changed, 258 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7575a797c0..32f2da2aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) * Node: Added XCLAIM command ([#2092](https://github.com/valkey-io/valkey-glide/pull/2092)) * Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) * Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index ef1b324437..992bc06190 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -106,6 +106,7 @@ function initialize() { GlideClientConfiguration, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, @@ -195,6 +196,7 @@ function initialize() { GlideClientConfiguration, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8353f79c29..08d6b61312 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2149,6 +2149,19 @@ export function createFunctionList( return createCommand(RequestType.FunctionList, args); } +/** Type of the response of `FUNCTION STATS` command. */ +export type FunctionStatsResponse = Record< + string, + | null + | Record + | Record> +>; + +/** @internal */ +export function createFunctionStats(): command_request.Command { + return createCommand(RequestType.FunctionStats, []); +} + /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 1c62b34506..daedf2935d 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -14,6 +14,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, InfoOptions, LolwutOptions, SortOptions, @@ -33,6 +34,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createInfo, createLastSave, createLolwut, @@ -545,6 +547,58 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionList(options)); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * @returns A `Record` with two keys: + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + * + * See example for more details. + * + * @example + * ```typescript + * const response = await client.functionStats(); + * console.log(response); // Output: + * // { + * // "running_script": + * // { + * // "name": "deep_thought", + * // "command": ["fcall", "deep_thought", "0"], + * // "duration_ms": 5008 + * // }, + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * // Output if no scripts running: + * // { + * // "running_script": null + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * ``` + */ + public async functionStats(): Promise { + return this.createWritePromise(createFunctionStats()); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 14f5e0f7fb..bfd00dbc5f 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -14,6 +14,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, InfoOptions, LolwutOptions, SortClusterOptions, @@ -35,6 +36,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createInfo, createLastSave, createLolwut, @@ -834,7 +836,7 @@ export class GlideClusterClient extends BaseClient { * since Valkey version 7.0.0. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @param route - The command will be routed to all primary node, unless `route` is provided, in which + * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. * @returns A simple OK response. * @@ -892,6 +894,65 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * @param route - The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed to all primary nodes. + * @returns A `Record` with two keys: + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + * + * See example for more details. + * + * @example + * ```typescript + * const response = await client.functionStats("randomNode"); + * console.log(response); // Output: + * // { + * // "running_script": + * // { + * // "name": "deep_thought", + * // "command": ["fcall", "deep_thought", "0"], + * // "duration_ms": 5008 + * // }, + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * // Output if no scripts running: + * // { + * // "running_script": null + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * ``` + */ + public async functionStats( + route?: Routes, + ): Promise> { + return this.createWritePromise( + createFunctionStats(), + toProtobufRoute(route), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 20e35b49ee..c39763be84 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -24,6 +24,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, // eslint-disable-line @typescript-eslint/no-unused-vars + FunctionStatsResponse, // eslint-disable-line @typescript-eslint/no-unused-vars GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -87,6 +88,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createGeoAdd, createGeoDist, createGeoHash, @@ -2492,6 +2494,23 @@ export class BaseTransaction> { return this.addAndReturn(createFunctionList(options)); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * Command Response - A `Record` of type {@link FunctionStatsResponse} with two keys: + * + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + */ + public functionStats(): T { + return this.addAndReturn(createFunctionStats()); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 11b75ef82e..a356812ece 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -25,6 +25,7 @@ import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { checkFunctionListResponse, + checkFunctionStatsResponse, convertStringArrayToBuffer, flushAndCloseClient, generateLuaLibCode, @@ -507,7 +508,7 @@ describe("GlideClient", () => { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "function load test_%p", + "function load function list function stats test_%p", async (protocol) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; @@ -534,6 +535,9 @@ describe("GlideClient", () => { await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); + let functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 1, 1); + let functionList = await client.functionList({ libNamePattern: libName, }); @@ -592,6 +596,9 @@ describe("GlideClient", () => { newCode, ); + functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 1, 2); + expect( await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); @@ -600,6 +607,8 @@ describe("GlideClient", () => { ).toEqual(2); } finally { expect(await client.functionFlush()).toEqual("OK"); + const functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 0, 0); client.close(); } }, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 1edb82e84b..04943619f2 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -25,11 +25,17 @@ import { ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, GeoUnit, SortOrder } from "../build-ts/src/Commands"; +import { + FlushMode, + FunctionStatsResponse, + GeoUnit, + SortOrder, +} from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, checkFunctionListResponse, + checkFunctionStatsResponse, flushAndCloseClient, generateLuaLibCode, getClientConfigurationOption, @@ -739,7 +745,7 @@ describe("GlideClusterClient", () => { "Single node route = %s", (singleNodeRoute) => { it( - "function load and function list", + "function load function list function stats", async () => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; @@ -775,6 +781,21 @@ describe("GlideClusterClient", () => { singleNodeRoute, (value) => expect(value).toEqual([]), ); + + let functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 0, + 0, + ), + ); + // load the library expect(await client.functionLoad(code)).toEqual( libName, @@ -803,6 +824,19 @@ describe("GlideClusterClient", () => { expectedFlags, ), ); + functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 1, + 1, + ), + ); // call functions from that library to confirm that it works let fcall = await client.fcallWithRoute( @@ -881,6 +915,19 @@ describe("GlideClusterClient", () => { newCode, ), ); + functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 1, + 2, + ), + ); fcall = await client.fcallWithRoute( func2Name, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 22283738f7..4d4d6d07d6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -19,6 +19,7 @@ import { ClusterTransaction, FlushMode, FunctionListResponse, + FunctionStatsResponse, GeoUnit, GeospatialData, GlideClient, @@ -396,6 +397,43 @@ export function checkFunctionListResponse( expect(hasLib).toBeTruthy(); } +/** + * Validate whether `FUNCTION STATS` response contains required info. + * + * @param response - The response from server. + * @param runningFunction - Command line of running function expected. Empty, if nothing expected. + * @param libCount - Expected libraries count. + * @param functionCount - Expected functions count. + */ +export function checkFunctionStatsResponse( + response: FunctionStatsResponse, + runningFunction: string[], + libCount: number, + functionCount: number, +) { + if (response.running_script === null && runningFunction.length > 0) { + fail("No running function info"); + } + + if (response.running_script !== null && runningFunction.length == 0) { + fail( + "Unexpected running function info: " + + (response.running_script.command as string[]).join(" "), + ); + } + + if (response.running_script !== null) { + expect(response.running_script.command).toEqual(runningFunction); + // command line format is: + // fcall|fcall_ro * * + expect(response.running_script.name).toEqual(runningFunction[1]); + } + + expect(response.engines).toEqual({ + LUA: { libraries_count: libCount, functions_count: functionCount }, + }); +} + /** * Check transaction response. * @param response - Transaction result received from `exec` call. @@ -1183,6 +1221,8 @@ export async function transactionTest( ); if (gte(version, "7.0.0")) { + baseTransaction.functionFlush(); + responseData.push(["functionFlush()", "OK"]); baseTransaction.functionLoad(code); responseData.push(["functionLoad(code)", libName]); baseTransaction.functionLoad(code, true); @@ -1196,6 +1236,14 @@ export async function transactionTest( 'fcallReadonly(funcName, [], ["one", "two"]', "one", ]); + baseTransaction.functionStats(); + responseData.push([ + "functionStats()", + { + running_script: null, + engines: { LUA: { libraries_count: 1, functions_count: 1 } }, + }, + ]); baseTransaction.functionDelete(libName); responseData.push(["functionDelete(libName)", "OK"]); baseTransaction.functionFlush(); From 1b5c73b0801cf77fa37dde5df3b12ec88edd3fa3 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 7 Aug 2024 09:59:28 -0700 Subject: [PATCH 151/236] Node: add `SRANDMEMBER` command (#2067) * Node: add SRANDMEMBER command --------- Signed-off-by: Shoham Elias Signed-off-by: Andrew Carbonetto Co-authored-by: Shoham Elias --- CHANGELOG.md | 3 +- node/src/BaseClient.ts | 59 +++++++++++++++++++++++++++++++++++++ node/src/Commands.ts | 11 +++++++ node/src/Transaction.ts | 26 ++++++++++++++++ node/tests/SharedTests.ts | 42 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 6 ++++ 6 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f2da2aff..40a961040a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,8 @@ * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) -* Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added SRANDMEMBER command ([#2067](https://github.com/valkey-io/valkey-glide/pull/2067)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) * Node: Added XDEL command ([#2064](https://github.com/valkey-io/valkey-glide/pull/2064)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5dcf0fed8b..b2ad5bb479 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -141,6 +141,7 @@ import { createSMembers, createSMove, createSPop, + createSRandMember, createSRem, createSUnion, createSUnionStore, @@ -2377,6 +2378,64 @@ export class BaseClient { ); } + /** + * Returns a random element from the set value stored at `key`. + * + * See https://valkey.io/commands/srandmember for more details. + * + * @param key - The key from which to retrieve the set member. + * @returns A random element from the set, or null if `key` does not exist. + * + * @example + * ```typescript + * // Example usage of srandmember method to return a random member from a set + * const result = await client.srandmember("my_set"); + * console.log(result); // Output: 'member1' - A random member of "my_set". + * ``` + * + * @example + * ```typescript + * // Example usage of srandmember method with non-existing key + * const result = await client.srandmember("non_existing_set"); + * console.log(result); // Output: null + * ``` + */ + public srandmember(key: string): Promise { + return this.createWritePromise(createSRandMember(key)); + } + + /** + * Returns one or more random elements from the set value stored at `key`. + * + * See https://valkey.io/commands/srandmember for more details. + * + * @param key - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If `count` is negative, allows for duplicates members. + * @returns a list of members from the set. If the set does not exist or is empty, an empty list will be returned. + * + * @example + * ```typescript + * // Example usage of srandmemberCount method to return multiple random members from a set + * const result = await client.srandmemberCount("my_set", -3); + * console.log(result); // Output: ['member1', 'member1', 'member2'] - Random members of "my_set". + * ``` + * + * @example + * ```typescript + * // Example usage of srandmemberCount method with non-existing key + * const result = await client.srandmemberCount("non_existing_set", 3); + * console.log(result); // Output: [] - An empty list since the key does not exist. + * ``` + */ + public async srandmemberCount( + key: string, + count: number, + ): Promise { + return this.createWritePromise(createSRandMember(key, count)); + } + /** Returns the number of keys in `keys` that exist in the database. * See https://valkey.io/commands/exists/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 08d6b61312..fec61115e5 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1168,6 +1168,17 @@ export function createSPop( return createCommand(RequestType.SPop, args); } +/** + * @internal + */ +export function createSRandMember( + key: string, + count?: number, +): command_request.Command { + const args: string[] = count == undefined ? [key] : [key, count.toString()]; + return createCommand(RequestType.SRandMember, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c39763be84..35f5a8abb7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -170,6 +170,7 @@ import { createSMembers, createSMove, createSPop, + createSRandMember, createSRem, createSUnion, createSUnionStore, @@ -1306,6 +1307,31 @@ export class BaseTransaction> { return this.addAndReturn(createSPop(key, count), true); } + /** Returns a random element from the set value stored at `key`. + * + * See https://valkey.io/commands/srandmember for more details. + * + * @param key - The key from which to retrieve the set member. + * Command Response - A random element from the set, or null if `key` does not exist. + */ + public srandmember(key: string): T { + return this.addAndReturn(createSRandMember(key)); + } + + /** Returns one or more random elements from the set value stored at `key`. + * + * See https://valkey.io/commands/srandmember for more details. + * + * @param key - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If `count` is negative, allows for duplicates members. + * Command Response - A list of members from the set. If the set does not exist or is empty, an empty list will be returned. + */ + public srandmemberCount(key: string, count: number): T { + return this.addAndReturn(createSRandMember(key, count)); + } + /** Returns the number of keys in `keys` that exist in the database. * See https://valkey.io/commands/exists/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index f41cd4a968..5ff459e5f5 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2656,6 +2656,48 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `srandmember and srandmemberCount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const members = ["member1", "member2", "member3"]; + expect(await client.sadd(key, members)).toEqual(3); + + const result2 = await client.srandmember(key); + expect(members).toContain(result2); + expect(await client.srandmember("nonExistingKey")).toEqual( + null, + ); + + // unique values are expected as count is positive + let result = await client.srandmemberCount(key, 4); + expect(result.length).toEqual(3); + expect(new Set(result)).toEqual(new Set(members)); + + // duplicate values are expected as count is negative + result = await client.srandmemberCount(key, -4); + expect(result.length).toEqual(4); + result.forEach((member) => { + expect(members).toContain(member); + }); + + // empty return values for non-existing or empty keys + result = await client.srandmemberCount(key, 0); + expect(result.length).toEqual(0); + expect(result).toEqual([]); + expect( + await client.srandmemberCount("nonExistingKey", 0), + ).toEqual([]); + + expect(await client.set(key, "value")).toBe("OK"); + await expect(client.srandmember(key)).rejects.toThrow(); + await expect(client.srandmemberCount(key, 2)).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `exists with existing keys, an non existing key_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4d4d6d07d6..fe6df3885d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -739,6 +739,12 @@ export async function transactionTest( baseTransaction.smembers(key7); responseData.push(["smembers(key7)", new Set(["bar"])]); + baseTransaction.srandmember(key7); + responseData.push(["srandmember(key7)", "bar"]); + baseTransaction.srandmemberCount(key7, 2); + responseData.push(["srandmemberCount(key7, 2)", ["bar"]]); + baseTransaction.srandmemberCount(key7, -2); + responseData.push(["srandmemberCount(key7, -2)", ["bar", "bar"]]); baseTransaction.spop(key7); responseData.push(["spop(key7)", "bar"]); baseTransaction.spopCount(key7, 2); From 225410167a0a4e3e30aa780a59f951f89b201871 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 10:07:13 -0700 Subject: [PATCH 152/236] Node: Add `HRANDFIELD` command. (#2096) * Add `HRANDFIELD` command. Signed-off-by: Yury-Fridlyand Signed-off-by: Andrew Carbonetto Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + .../glide/api/commands/HashBaseCommands.java | 2 +- .../test/java/glide/SharedCommandTests.java | 2 +- node/src/BaseClient.ts | 76 +++++++++++++++++++ node/src/Commands.ts | 12 +++ node/src/Transaction.ts | 57 ++++++++++++++ node/tests/SharedTests.ts | 57 ++++++++++++++ node/tests/TestUtilities.ts | 11 ++- 8 files changed, 215 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a961040a..9f92d1fb04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added HRANDFIELD command ([#2096](https://github.com/valkey-io/valkey-glide/pull/2096)) * Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) * Node: Added XCLAIM command ([#2092](https://github.com/valkey-io/valkey-glide/pull/2092)) * Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) diff --git a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java index 5bea78fed9..9140932fc0 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -591,7 +591,7 @@ public interface HashBaseCommands { */ CompletableFuture hrandfieldWithCountWithValues(String key, long count); - /* + /** * Retrieves up to count random field names along with their values from the hash * value stored at key. * diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index f4256bf4fb..76b367a773 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1438,7 +1438,7 @@ public void hrandfield(BaseClient client) { assertEquals(data.get(pair[0]), pair[1]); } - // Key exists, but it is not a List + // Key exists, but it is not a hash assertEquals(OK, client.set(key2, "value").get()); Exception executionException = assertThrows(ExecutionException.class, () -> client.hrandfield(key2).get()); diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b2ad5bb479..965dafb7b6 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -85,6 +85,7 @@ import { createHIncrByFloat, createHLen, createHMGet, + createHRandField, createHSet, createHSetNX, createHStrlen, @@ -1593,6 +1594,81 @@ export class BaseClient { return this.createWritePromise(createHStrlen(key, field)); } + /** + * Returns a random field name from the hash value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * @returns A random field name from the hash stored at `key`, or `null` when + * the key does not exist. + * + * @example + * ```typescript + * console.log(await client.hrandfield("myHash")); // Output: 'field' + * ``` + */ + public async hrandfield(key: string): Promise { + return this.createWritePromise(createHRandField(key)); + } + + /** + * Retrieves up to `count` random field names from the hash value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * @param count - The number of field names to return. + * + * If `count` is positive, returns unique elements. If negative, allows for duplicates. + * @returns An `array` of random field names from the hash stored at `key`, + * or an `empty array` when the key does not exist. + * + * @example + * ```typescript + * console.log(await client.hrandfieldCount("myHash", 2)); // Output: ['field1', 'field2'] + * ``` + */ + public async hrandfieldCount( + key: string, + count: number, + ): Promise { + return this.createWritePromise(createHRandField(key, count)); + } + + /** + * Retrieves up to `count` random field names along with their values from the hash + * value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * @param count - The number of field names to return. + * + * If `count` is positive, returns unique elements. If negative, allows for duplicates. + * @returns A 2D `array` of `[fieldName, value]` `arrays`, where `fieldName` is a random + * field name from the hash and `value` is the associated value of the field name. + * If the hash does not exist or is empty, the response will be an empty `array`. + * + * @example + * ```typescript + * const result = await client.hrandfieldCountWithValues("myHash", 2); + * console.log(result); // Output: [['field1', 'value1'], ['field2', 'value2']] + * ``` + */ + public async hrandfieldWithValues( + key: string, + count: number, + ): Promise<[string, string][]> { + return this.createWritePromise(createHRandField(key, count, true)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index fec61115e5..94f9eb758a 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3166,6 +3166,18 @@ export function createHStrlen( return createCommand(RequestType.HStrlen, [key, field]); } +/** @internal */ +export function createHRandField( + key: string, + count?: number, + withValues?: boolean, +): command_request.Command { + const args = [key]; + if (count !== undefined) args.push(count.toString()); + if (withValues) args.push("WITHVALUES"); + return createCommand(RequestType.HRandField, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 35f5a8abb7..0a959352f3 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -106,6 +106,7 @@ import { createHIncrByFloat, createHLen, createHMGet, + createHRandField, createHSet, createHSetNX, createHStrlen, @@ -834,6 +835,62 @@ export class BaseTransaction> { return this.addAndReturn(createHStrlen(key, field)); } + /** + * Returns a random field name from the hash value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * + * Command Response - A random field name from the hash stored at `key`, or `null` when + * the key does not exist. + */ + public hrandfield(key: string): T { + return this.addAndReturn(createHRandField(key)); + } + + /** + * Retrieves up to `count` random field names from the hash value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * @param count - The number of field names to return. + * + * If `count` is positive, returns unique elements. If negative, allows for duplicates. + * + * Command Response - An `array` of random field names from the hash stored at `key`, + * or an `empty array` when the key does not exist. + */ + public hrandfieldCount(key: string, count: number): T { + return this.addAndReturn(createHRandField(key, count)); + } + + /** + * Retrieves up to `count` random field names along with their values from the hash + * value stored at `key`. + * + * See https://valkey.io/commands/hrandfield/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the hash. + * @param count - The number of field names to return. + * + * If `count` is positive, returns unique elements. If negative, allows for duplicates. + * + * Command Response - A 2D `array` of `[fieldName, value]` `arrays`, where `fieldName` is a random + * field name from the hash and `value` is the associated value of the field name. + * If the hash does not exist or is empty, the response will be an empty `array`. + */ + public hrandfieldWithValues(key: string, count: number): T { + return this.addAndReturn(createHRandField(key, count, true)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5ff459e5f5..125c63d4a7 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1532,6 +1532,63 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `hrandfield test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = uuidv4(); + const key2 = uuidv4(); + + // key does not exist + expect(await client.hrandfield(key1)).toBeNull(); + expect(await client.hrandfieldCount(key1, 5)).toEqual([]); + expect(await client.hrandfieldWithValues(key1, 5)).toEqual([]); + + const data = { "f 1": "v 1", "f 2": "v 2", "f 3": "v 3" }; + const fields = Object.keys(data); + const entries = Object.entries(data); + expect(await client.hset(key1, data)).toEqual(3); + + expect(fields).toContain(await client.hrandfield(key1)); + + // With Count - positive count + let result = await client.hrandfieldCount(key1, 5); + expect(result).toEqual(fields); + + // With Count - negative count + result = await client.hrandfieldCount(key1, -5); + expect(result.length).toEqual(5); + result.map((r) => expect(fields).toContain(r)); + + // With values - positive count + let result2 = await client.hrandfieldWithValues(key1, 5); + expect(result2).toEqual(entries); + + // With values - negative count + result2 = await client.hrandfieldWithValues(key1, -5); + expect(result2.length).toEqual(5); + result2.map((r) => expect(entries).toContainEqual(r)); + + // key exists but holds non hash type value + expect(await client.set(key2, "value")).toEqual("OK"); + await expect(client.hrandfield(key2)).rejects.toThrow( + RequestError, + ); + await expect(client.hrandfieldCount(key2, 42)).rejects.toThrow( + RequestError, + ); + await expect( + client.hrandfieldWithValues(key2, 42), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lpush, lpop and lrange with existing and non existing key_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fe6df3885d..1a17cba376 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -489,7 +489,7 @@ export async function transactionTest( const key1 = "{key}" + uuidv4(); // string const key2 = "{key}" + uuidv4(); // string const key3 = "{key}" + uuidv4(); // string - const key4 = "{key}" + uuidv4(); + const key4 = "{key}" + uuidv4(); // hash const key5 = "{key}" + uuidv4(); const key6 = "{key}" + uuidv4(); const key7 = "{key}" + uuidv4(); @@ -586,6 +586,12 @@ export async function transactionTest( responseData.push(["hstrlen(key4, field)", value.length]); baseTransaction.hlen(key4); responseData.push(["hlen(key4)", 1]); + baseTransaction.hrandfield(key4); + responseData.push(["hrandfield(key4)", field]); + baseTransaction.hrandfieldCount(key4, -2); + responseData.push(["hrandfieldCount(key4, -2)", [field, field]]); + baseTransaction.hrandfieldWithValues(key4, 2); + responseData.push(["hrandfieldWithValues(key4, 2)", [[field, value]]]); baseTransaction.hsetnx(key4, field, value); responseData.push(["hsetnx(key4, field, value)", false]); baseTransaction.hvals(key4); @@ -600,6 +606,9 @@ export async function transactionTest( responseData.push(["hmget(key4, [field])", [null]]); baseTransaction.hexists(key4, field); responseData.push(["hexists(key4, field)", false]); + baseTransaction.hrandfield(key4); + responseData.push(["hrandfield(key4)", null]); + baseTransaction.lpush(key5, [ field + "1", field + "2", From 74812eba7dbe1320736e0addf5ffcde0400e1e9d Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 7 Aug 2024 10:08:49 -0700 Subject: [PATCH 153/236] Node: added APPEND command (#2095) * Node: added APPEND command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 27 +++++++++++++++++++++++++++ node/src/Commands.ts | 8 ++++++++ node/src/Transaction.ts | 16 ++++++++++++++++ node/tests/SharedTests.ts | 23 +++++++++++++++++++++++ node/tests/TestUtilities.ts | 6 +++++- 6 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f92d1fb04..c30b3f7f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ * Node: Added SRANDMEMBER command ([#2067](https://github.com/valkey-io/valkey-glide/pull/2067)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) +* Node: Added APPEND command ([#2095](https://github.com/valkey-io/valkey-glide/pull/2095)) * Node: Added XDEL command ([#2064](https://github.com/valkey-io/valkey-glide/pull/2064)) * Node: Added LMPOP & BLMPOP command ([#2050](https://github.com/valkey-io/valkey-glide/pull/2050)) * Node: Added PUBSUB support ([#1964](https://github.com/valkey-io/valkey-glide/pull/1964)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 965dafb7b6..6534387a7e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -49,6 +49,7 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createAppend, createBLMPop, createBLMove, createBLPop, @@ -4983,6 +4984,32 @@ export class BaseClient { return this.createWritePromise(createSetRange(key, offset, value)); } + /** + * Appends a `value` to a `key`. If `key` does not exist it is created and set as an empty string, + * so `APPEND` will be similar to {@link set} in this special case. + * + * See https://valkey.io/commands/append/ for more details. + * + * @param key - The key of the string. + * @param value - The key of the string. + * @returns The length of the string after appending the value. + * + * @example + * ```typescript + * const len = await client.append("key", "Hello"); + * console.log(len); + * // Output: 5 - Indicates that "Hello" has been appended to the value of "key", which was initially + * // empty, resulting in a new value of "Hello" with a length of 5 - similar to the set operation. + * len = await client.append("key", " world"); + * console.log(result); + * // Output: 11 - Indicates that " world" has been appended to the value of "key", resulting in a + * // new value of "Hello world" with a length of 11. + * ``` + */ + public async append(key: string, value: string): Promise { + return this.createWritePromise(createAppend(key, value)); + } + /** * Pops one or more elements from the first non-empty list from the provided `keys`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 94f9eb758a..9a4073c29e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3304,6 +3304,14 @@ export function createSetRange( return createCommand(RequestType.SetRange, [key, offset.toString(), value]); } +/** @internal */ +export function createAppend( + key: string, + value: string, +): command_request.Command { + return createCommand(RequestType.Append, [key, value]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0a959352f3..61e3313571 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -54,6 +54,7 @@ import { StreamReadOptions, StreamTrimOptions, ZAddOptions, + createAppend, createBLMPop, createBLMove, createBLPop, @@ -3036,6 +3037,21 @@ export class BaseTransaction> { return this.addAndReturn(createSetRange(key, offset, value)); } + /** + * Appends a `value` to a `key`. If `key` does not exist it is created and set as an empty string, + * so `APPEND` will be similar to {@link set} in this special case. + * + * See https://valkey.io/commands/append/ for more details. + * + * @param key - The key of the string. + * @param value - The key of the string. + * + * Command Response - The length of the string after appending the value. + */ + public append(key: string, value: string): T { + return this.addAndReturn(createAppend(key, value)); + } + /** * Pops one or more elements from the first non-empty list from the provided `keys`. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 125c63d4a7..0937328337 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4850,6 +4850,29 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "append test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const value = uuidv4(); + + // Append on non-existing string(similar to SET) + expect(await client.append(key1, value)).toBe(value.length); + expect(await client.append(key1, value)).toBe(value.length * 2); + expect(await client.get(key1)).toEqual(value.concat(value)); + + // key exists but holding the wrong kind of value + expect(await client.sadd(key2, ["a"])).toBe(1); + await expect(client.append(key2, "_")).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + // Set command tests async function setWithExpiryOptions(client: BaseClient) { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 1a17cba376..7849856399 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -577,7 +577,11 @@ export async function transactionTest( baseTransaction.strlen(key1); responseData.push(["strlen(key1)", 3]); baseTransaction.setrange(key1, 0, "GLIDE"); - responseData.push(["setrange(key1, 0, 'GLIDE'", 5]); + responseData.push(["setrange(key1, 0, 'GLIDE')", 5]); + baseTransaction.del([key1]); + responseData.push(["del([key1])", 1]); + baseTransaction.append(key1, "bar"); + responseData.push(["append(key1, value)", 3]); baseTransaction.del([key1]); responseData.push(["del([key1])", 1]); baseTransaction.hset(key4, { [field]: value }); From e89048c3a66bbb39ad815db4d4a53f1a5c625916 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:17:44 -0700 Subject: [PATCH 154/236] Node: add command BZPOPMAX & BZPOPMIN (#2077) * Node: add command BZPOPMAX & BZPOPMIN Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 58 ++++++++++++++++++ node/src/Commands.ts | 20 +++++++ node/src/Transaction.ts | 40 +++++++++++++ node/tests/GlideClient.test.ts | 27 ++++----- node/tests/GlideClusterClient.test.ts | 2 + node/tests/SharedTests.ts | 86 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 8 +++ 8 files changed, 227 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30b3f7f67..190ebcae39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ * Node: Added PUBSUB * commands ([#2090](https://github.com/valkey-io/valkey-glide/pull/2090)) * Python: Added PUBSUB * commands ([#2043](https://github.com/valkey-io/valkey-glide/pull/2043)) * Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) +* Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 6534387a7e..fb4b28bea7 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -55,6 +55,8 @@ import { createBLPop, createBRPop, createBZMPop, + createBZPopMax, + createBZPopMin, createBitCount, createBitField, createBitOp, @@ -3417,6 +3419,34 @@ export class BaseClient { return this.createWritePromise(createZPopMin(key, count)); } + /** + * Blocks the connection until it removes and returns a member with the lowest score from the + * first non-empty sorted set, with the given `key` being checked in the order they + * are provided. + * `BZPOPMIN` is the blocking variant of {@link zpopmin}. + * + * See https://valkey.io/commands/bzpopmin/ for more details. + * + * @remarks When in cluster mode, `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of + * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * @returns An `array` containing the key where the member was popped out, the member, itself, and the member score. + * If no member could be popped and the `timeout` expired, returns `null`. + * + * @example + * ```typescript + * const data = await client.bzpopmin(["zset1", "zset2"], 0.5); + * console.log(data); // Output: ["zset1", "a", 2]; + * ``` + */ + public async bzpopmin( + keys: string[], + timeout: number, + ): Promise<[string, string, number] | null> { + return this.createWritePromise(createBZPopMin(keys, timeout)); + } + /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. @@ -3449,6 +3479,34 @@ export class BaseClient { return this.createWritePromise(createZPopMax(key, count)); } + /** + * Blocks the connection until it removes and returns a member with the highest score from the + * first non-empty sorted set, with the given `key` being checked in the order they + * are provided. + * `BZPOPMAX` is the blocking variant of {@link zpopmax}. + * + * See https://valkey.io/commands/zpopmax/ for more details. + * + * @remarks When in cluster mode, `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of + * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * @returns An `array` containing the key where the member was popped out, the member, itself, and the member score. + * If no member could be popped and the `timeout` expired, returns `null`. + * + * @example + * ```typescript + * const data = await client.bzpopmax(["zset1", "zset2"], 0.5); + * console.log(data); // Output: ["zset1", "c", 2]; + * ``` + */ + public async bzpopmax( + keys: string[], + timeout: number, + ): Promise<[string, string, number] | null> { + return this.createWritePromise(createBZPopMax(keys, timeout)); + } + /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. * See https://valkey.io/commands/pttl for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 9a4073c29e..f50d98e4d2 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3396,3 +3396,23 @@ export function createPubSubShardNumSub( ): command_request.Command { return createCommand(RequestType.PubSubSNumSub, channels ? channels : []); } + +/** + * @internal + */ +export function createBZPopMax( + keys: string[], + timeout: number, +): command_request.Command { + return createCommand(RequestType.BZPopMax, [...keys, timeout.toString()]); +} + +/** + * @internal + */ +export function createBZPopMin( + keys: string[], + timeout: number, +): command_request.Command { + return createCommand(RequestType.BZPopMin, [...keys, timeout.toString()]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 61e3313571..056997325e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -60,6 +60,8 @@ import { createBLPop, createBRPop, createBZMPop, + createBZPopMax, + createBZPopMin, createBitCount, createBitField, createBitOp, @@ -1885,6 +1887,25 @@ export class BaseTransaction> { return this.addAndReturn(createZPopMin(key, count)); } + /** + * Blocks the connection until it removes and returns a member with the lowest score from the + * first non-empty sorted set, with the given `key` being checked in the order they + * are provided. + * `BZPOPMIN` is the blocking variant of {@link zpopmin}. + * + * See https://valkey.io/commands/bzpopmin/ for more details. + * + * @param keys - The keys of the sorted sets. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of + * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * + * Command Response - An `array` containing the key where the member was popped out, the member, itself, and the member score. + * If no member could be popped and the `timeout` expired, returns `null`. + */ + public bzpopmin(keys: string[], timeout: number): T { + return this.addAndReturn(createBZPopMin(keys, timeout)); + } + /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. @@ -1901,6 +1922,25 @@ export class BaseTransaction> { return this.addAndReturn(createZPopMax(key, count)); } + /** + * Blocks the connection until it removes and returns a member with the highest score from the + * first non-empty sorted set, with the given `key` being checked in the order they + * are provided. + * `BZPOPMAX` is the blocking variant of {@link zpopmax}. + * + * See https://valkey.io/commands/bzpopmax/ for more details. + * + * @param keys - The keys of the sorted sets. + * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of + * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * + * Command Response - An `array` containing the key where the member was popped out, the member, itself, and the member score. + * If no member could be popped and the `timeout` expired, returns `null`. + */ + public bzpopmax(keys: string[], timeout: number): T { + return this.addAndReturn(createBZPopMax(keys, timeout)); + } + /** Echoes the provided `message` back. * See https://valkey.io/commands/echo for more details. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index a356812ece..eb4f0d793d 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -143,21 +143,18 @@ describe("GlideClient", () => { ), ); - const blmovePromise = client.blmove( - "source", - "destination", - ListDirection.LEFT, - ListDirection.LEFT, - 0.1, - ); - - const blmpopPromise = client.blmpop( - ["key1", "key2"], - ListDirection.LEFT, - 0.1, - ); - - const promiseList = [blmovePromise, blmpopPromise]; + const promiseList = [ + client.blmove( + "source", + "destination", + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ), + client.blmpop(["key1", "key2"], ListDirection.LEFT, 0.1), + client.bzpopmax(["key1", "key2"], 0), + client.bzpopmin(["key1", "key2"], 0), + ]; try { for (const promise of promiseList) { diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 04943619f2..7165837804 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -345,6 +345,8 @@ describe("GlideClusterClient", () => { client.sortStore("abc", "zyx", { isAlpha: true }), client.lmpop(["abc", "def"], ListDirection.LEFT, 1), client.blmpop(["abc", "def"], ListDirection.RIGHT, 0.1, 1), + client.bzpopmax(["abc", "def"], 0.5), + client.bzpopmin(["abc", "def"], 0.5), ]; if (gte(cluster.getVersion(), "6.2.0")) { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0937328337..dcab05c55e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4082,6 +4082,92 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bzpopmax test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + + expect(await client.zadd(key1, { a: 1.0, b: 1.5 })).toBe(2); + expect(await client.zadd(key2, { c: 2.0 })).toBe(1); + expect(await client.bzpopmax([key1, key2], 0.5)).toEqual([ + key1, + "b", + 1.5, + ]); + + // nothing popped out / key does not exist + expect( + await client.bzpopmax( + [key3], + cluster.checkIfServerVersionLessThan("6.0.0") + ? 1.0 + : 0.001, + ), + ).toBeNull(); + + // pops from the second key + expect(await client.bzpopmax([key3, key2], 0.5)).toEqual([ + key2, + "c", + 2.0, + ]); + + // key exists but holds non-ZSET value + expect(await client.set(key3, "bzpopmax")).toBe("OK"); + await expect(client.bzpopmax([key3], 0.5)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bzpopmin test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + + expect(await client.zadd(key1, { a: 1.0, b: 1.5 })).toBe(2); + expect(await client.zadd(key2, { c: 2.0 })).toBe(1); + expect(await client.bzpopmin([key1, key2], 0.5)).toEqual([ + key1, + "a", + 1.0, + ]); + + // nothing popped out / key does not exist + expect( + await client.bzpopmin( + [key3], + cluster.checkIfServerVersionLessThan("6.0.0") + ? 1.0 + : 0.001, + ), + ).toBeNull(); + + // pops from the second key + expect(await client.bzpopmin([key3, key2], 0.5)).toEqual([ + key2, + "c", + 2.0, + ]); + + // key exists but holds non-ZSET value + expect(await client.set(key3, "bzpopmin")).toBe("OK"); + await expect(client.bzpopmin([key3], 0.5)).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `Pttl test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 7849856399..a2816ceaf5 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -861,6 +861,14 @@ export async function transactionTest( responseData.push(["zpopmin(key8)", { member2: 3.0 }]); baseTransaction.zpopmax(key8); responseData.push(["zpopmax(key8)", { member5: 5 }]); + baseTransaction.zadd(key8, { member6: 6 }); + responseData.push(["zadd(key8, {member6: 6})", 1]); + baseTransaction.bzpopmax([key8], 0.5); + responseData.push(["bzpopmax([key8], 0.5)", [key8, "member6", 6]]); + baseTransaction.zadd(key8, { member7: 1 }); + responseData.push(["zadd(key8, {member7: 1})", 1]); + baseTransaction.bzpopmin([key8], 0.5); + responseData.push(["bzpopmin([key8], 0.5)", [key8, "member7", 1]]); baseTransaction.zremRangeByRank(key8, 1, 1); responseData.push(["zremRangeByRank(key8, 1, 1)", 1]); baseTransaction.zremRangeByScore( From d58681ce8c7d50af863963ecc569c7f52c347418 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 10:40:56 -0700 Subject: [PATCH 155/236] Node: Add `XINFO CONSUMERS` command. (#2093) * Add `XINFO CONSUMERS` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Guian Gumpac --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 34 +++++++++ node/src/Commands.ts | 8 ++ node/src/Transaction.ts | 17 +++++ node/tests/SharedTests.ts | 146 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 6 files changed, 208 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 190ebcae39..1b1cb08fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XINFO CONSUMERS command ([#2093](https://github.com/valkey-io/valkey-glide/pull/2093)) * Node: Added HRANDFIELD command ([#2096](https://github.com/valkey-io/valkey-glide/pull/2096)) * Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) * Node: Added XCLAIM command ([#2092](https://github.com/valkey-io/valkey-glide/pull/2092)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index fb4b28bea7..5f7796c560 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -162,6 +162,7 @@ import { createXDel, createXGroupCreate, createXGroupDestroy, + createXInfoConsumers, createXLen, createXRead, createXTrim, @@ -3872,6 +3873,39 @@ export class BaseClient { return this.createWritePromise(createXLen(key)); } + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at `key`. + * + * See https://valkey.io/commands/xinfo-consumers/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @returns An `Array` of `Records`, where each mapping contains the attributes + * of a consumer for the given consumer group of the stream at `key`. + * + * @example + * ```typescript + * const result = await client.xinfoConsumers("my_stream", "my_group"); + * console.log(result); // Output: + * // [ + * // { + * // "name": "Alice", + * // "pending": 1, + * // "idle": 9104628, + * // "inactive": 18104698 // Added in 7.2.0 + * // }, + * // ... + * // ] + * ``` + */ + public async xinfoConsumers( + key: string, + group: string, + ): Promise[]> { + return this.createWritePromise(createXInfoConsumers(key, group)); + } + /** * Changes the ownership of a pending message. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f50d98e4d2..86fb433184 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2336,6 +2336,14 @@ export function createXLen(key: string): command_request.Command { return createCommand(RequestType.XLen, [key]); } +/** @internal */ +export function createXInfoConsumers( + key: string, + group: string, +): command_request.Command { + return createCommand(RequestType.XInfoConsumers, [key, group]); +} + /** Optional parameters for {@link BaseClient.xclaim|xclaim} command. */ export type StreamClaimOptions = { /** diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 056997325e..7aedf5ea36 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -193,6 +193,7 @@ import { createXAdd, createXClaim, createXDel, + createXInfoConsumers, createXLen, createXRead, createXTrim, @@ -2258,6 +2259,22 @@ export class BaseTransaction> { return this.addAndReturn(createXLen(key)); } + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at `key`. + * + * See https://valkey.io/commands/xinfo-consumers/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * + * Command Response - An `Array` of `Records`, where each mapping contains the attributes + * of a consumer for the given consumer group of the stream at `key`. + */ + public xinfoConsumers(key: string, group: string): T { + return this.addAndReturn(createXInfoConsumers(key, group)); + } + /** * Changes the ownership of a pending message. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index dcab05c55e..c19c896000 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -7081,6 +7081,152 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xinfoconsumers xinfo consumers %p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = uuidv4(); + const stringKey = uuidv4(); + const groupName1 = uuidv4(); + const consumer1 = uuidv4(); + const consumer2 = uuidv4(); + const streamId1 = "0-1"; + const streamId2 = "0-2"; + const streamId3 = "0-3"; + const streamId4 = "0-4"; + + expect( + await client.xadd( + key, + [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + { id: streamId1 }, + ), + ).toEqual(streamId1); + + expect( + await client.xadd( + key, + [ + ["entry2_field1", "entry2_value1"], + ["entry2_field2", "entry2_value2"], + ], + { id: streamId2 }, + ), + ).toEqual(streamId2); + + expect( + await client.xadd( + key, + [["entry3_field1", "entry3_value1"]], + { id: streamId3 }, + ), + ).toEqual(streamId3); + + expect( + await client.xgroupCreate(key, groupName1, "0-0"), + ).toEqual("OK"); + expect( + await client.customCommand([ + "XREADGROUP", + "GROUP", + groupName1, + consumer1, + "COUNT", + "1", + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + [streamId1]: [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + }, + }); + // Sleep to ensure the idle time value and inactive time value returned by xinfo_consumers is > 0 + await new Promise((resolve) => setTimeout(resolve, 2000)); + let result = await client.xinfoConsumers(key, groupName1); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual(consumer1); + expect(result[0].pending).toEqual(1); + expect(result[0].idle).toBeGreaterThan(0); + + if (cluster.checkIfServerVersionLessThan("7.2.0")) { + expect(result[0].inactive).toBeGreaterThan(0); + } + + expect( + await client.customCommand([ + "XGROUP", + "CREATECONSUMER", + key, + groupName1, + consumer2, + ]), + ).toBeTruthy(); + expect( + await client.customCommand([ + "XREADGROUP", + "GROUP", + groupName1, + consumer2, + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + [streamId2]: [ + ["entry2_field1", "entry2_value1"], + ["entry2_field2", "entry2_value2"], + ], + [streamId3]: [["entry3_field1", "entry3_value1"]], + }, + }); + + // Verify that xinfo_consumers contains info for 2 consumers now + result = await client.xinfoConsumers(key, groupName1); + expect(result.length).toEqual(2); + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xinfoConsumers(stringKey, "_"), + ).rejects.toThrow(RequestError); + + // Passing a non-existing key raises an error + const key2 = uuidv4(); + await expect(client.xinfoConsumers(key2, "_")).rejects.toThrow( + RequestError, + ); + + expect( + await client.xadd(key2, [["field", "value"]], { + id: streamId4, + }), + ).toEqual(streamId4); + + // Passing a non-existing group raises an error + await expect(client.xinfoConsumers(key2, "_")).rejects.toThrow( + RequestError, + ); + + expect( + await client.xgroupCreate(key2, groupName1, "0-0"), + ).toEqual("OK"); + expect(await client.xinfoConsumers(key2, groupName1)).toEqual( + [], + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `xclaim test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a2816ceaf5..24d4611df2 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -952,6 +952,8 @@ export async function transactionTest( 'xgroupCreate(key9, groupName2, "0-0", { mkStream: true })', "OK", ]); + baseTransaction.xinfoConsumers(key9, groupName1); + responseData.push(["xinfoConsumers(key9, groupName1)", []]); baseTransaction.xdel(key9, ["0-3", "0-5"]); responseData.push(["xdel(key9, [['0-3', '0-5']])", 1]); From fc75778d35173668e0b24cd7a8495fa0d76ebf4a Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 7 Aug 2024 16:01:09 -0700 Subject: [PATCH 156/236] Node: Add `XINFO STREAM` command (#2083) * Node: Add XINFO STREAM command --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 3 +- node/npm/glide/index.ts | 4 + node/src/BaseClient.ts | 72 +++++++++++ node/src/Commands.ts | 35 +++++ node/src/Transaction.ts | 18 +++ node/tests/GlideClient.test.ts | 55 ++++++++ node/tests/SharedTests.ts | 226 ++++++++++++++++++++++++++++++++- 7 files changed, 405 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1cb08fed..8b7bde5852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,9 @@ * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) -* Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added SRANDMEMBER command ([#2067](https://github.com/valkey-io/valkey-glide/pull/2067)) +* Node: Added XINFO STREAM command ([#2083](https://github.com/valkey-io/valkey-glide/pull/2083)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) * Node: Added SETRANGE command ([#2066](https://github.com/valkey-io/valkey-glide/pull/2066)) * Node: Added APPEND command ([#2095](https://github.com/valkey-io/valkey-glide/pull/2095)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 992bc06190..a78c0bdd0c 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -146,6 +146,8 @@ function initialize() { ExecAbortError, RedisError, ReturnType, + StreamEntries, + ReturnTypeXinfoStream, RequestError, TimeoutError, ConnectionError, @@ -199,6 +201,8 @@ function initialize() { FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, + StreamEntries, + ReturnTypeXinfoStream, RouteByAddress, Routes, SingleNodeRoute, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5f7796c560..4096424b2e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -40,6 +40,7 @@ import { RangeByIndex, RangeByLex, RangeByScore, + ReturnTypeXinfoStream, ScoreBoundary, ScoreFilter, SearchOrigin, @@ -163,6 +164,7 @@ import { createXGroupCreate, createXGroupDestroy, createXInfoConsumers, + createXInfoStream, createXLen, createXRead, createXTrim, @@ -4026,6 +4028,76 @@ export class BaseClient { return this.createWritePromise(createXGroupDestroy(key, groupName)); } + /** + * Returns information about the stream stored at `key`. + * + * @param key - The key of the stream. + * @param fullOptions - If `true`, returns verbose information with a limit of the first 10 PEL entries. + * If `number` is specified, returns verbose information limiting the returned PEL entries. + * If `0` is specified, returns verbose information with no limit. + * @returns A {@link ReturnTypeXinfoStream} of detailed stream information for the given `key`. See + * the example for a sample response. + * @example + * ```typescript + * const infoResult = await client.xinfoStream("my_stream"); + * console.log(infoResult); + * // Output: { + * // length: 2, + * // 'radix-tree-keys': 1, + * // 'radix-tree-nodes': 2, + * // 'last-generated-id': '1719877599564-1', + * // 'max-deleted-entry-id': '0-0', + * // 'entries-added': 2, + * // 'recorded-first-entry-id': '1719877599564-0', + * // 'first-entry': [ '1719877599564-0', ['some_field", "some_value', ...] ], + * // 'last-entry': [ '1719877599564-0', ['some_field", "some_value', ...] ], + * // groups: 1, + * // } + * ``` + * + * @example + * ```typescript + * const infoResult = await client.xinfoStream("my_stream", true); // default limit of 10 entries + * const infoResult = await client.xinfoStream("my_stream", 15); // limit of 15 entries + * console.log(infoResult); + * // Output: { + * // length: 2, + * // 'radix-tree-keys': 1, + * // 'radix-tree-nodes': 2, + * // 'last-generated-id': '1719877599564-1', + * // 'max-deleted-entry-id': '0-0', + * // 'entries-added': 2, + * // 'recorded-first-entry-id': '1719877599564-0', + * // entries: [ [ '1719877599564-0', ['some_field", "some_value', ...] ] ], + * // groups: [ { + * // name: 'group', + * // 'last-delivered-id': '1719877599564-0', + * // 'entries-read': 1, + * // lag: 1, + * // 'pel-count': 1, + * // pending: [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], + * // consumers: [ { + * // name: 'consumer', + * // 'seen-time': 1722624726802, + * // 'active-time': 1722624726802, + * // 'pel-count': 1, + * // pending: [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], + * // } + * // ] + * // } + * // ] + * // } + * ``` + */ + public async xinfoStream( + key: string, + fullOptions?: boolean | number, + ): Promise { + return this.createWritePromise( + createXInfoStream(key, fullOptions ?? false), + ); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 86fb433184..d20e029e67 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2329,6 +2329,41 @@ export function createXRead( return createCommand(RequestType.XRead, args); } +/** + * Represents a the return type for XInfo Stream in the response + */ +export type ReturnTypeXinfoStream = { + [key: string]: + | StreamEntries + | Record[]>[]; +}; + +/** + * Represents an array of Stream Entires in the response + */ +export type StreamEntries = string | number | (string | number | string[])[][]; + +/** + * @internal + */ +export function createXInfoStream( + key: string, + options: boolean | number, +): command_request.Command { + const args: string[] = [key]; + + if (options != false) { + args.push("FULL"); + + if (typeof options === "number") { + args.push("COUNT"); + args.push(options.toString()); + } + } + + return createCommand(RequestType.XInfoStream, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7aedf5ea36..4d7f8d81cb 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3,6 +3,7 @@ */ import { + BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; @@ -42,6 +43,7 @@ import { RangeByIndex, RangeByLex, RangeByScore, + ReturnTypeXinfoStream, // eslint-disable-line @typescript-eslint/no-unused-vars ScoreBoundary, ScoreFilter, SearchOrigin, @@ -194,6 +196,7 @@ import { createXClaim, createXDel, createXInfoConsumers, + createXInfoStream, createXLen, createXRead, createXTrim, @@ -2219,6 +2222,21 @@ export class BaseTransaction> { return this.addAndReturn(createXTrim(key, options)); } + /** + * Returns information about the stream stored at `key`. + * + * @param key - The key of the stream. + * @param fullOptions - If `true`, returns verbose information with a limit of the first 10 PEL entries. + * If `number` is specified, returns verbose information limiting the returned PEL entries. + * If `0` is specified, returns verbose information with no limit. + * + * Command Response - A {@link ReturnTypeXinfoStream} of detailed stream information for the given `key`. + * See example of {@link BaseClient.xinfoStream} for more details. + */ + public xinfoStream(key: string, fullOptions?: boolean | number): T { + return this.addAndReturn(createXInfoStream(key, fullOptions ?? false)); + } + /** Returns the server time. * See https://valkey.io/commands/time/ for details. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index eb4f0d793d..d74776ced0 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -1053,6 +1053,61 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "xinfo stream transaction test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + + const transaction = new Transaction(); + transaction.xadd(key, [["field1", "value1"]], { id: "0-1" }); + transaction.xinfoStream(key); + transaction.xinfoStream(key, true); + const result = await client.exec(transaction); + expect(result).not.toBeNull(); + + const versionLessThan7 = + cluster.checkIfServerVersionLessThan("7.0.0"); + + const expectedXinfoStreamResult = { + length: 1, + "radix-tree-keys": 1, + "radix-tree-nodes": 2, + "last-generated-id": "0-1", + groups: 0, + "first-entry": ["0-1", ["field1", "value1"]], + "last-entry": ["0-1", ["field1", "value1"]], + "max-deleted-entry-id": versionLessThan7 ? undefined : "0-0", + "entries-added": versionLessThan7 ? undefined : 1, + "recorded-first-entry-id": versionLessThan7 ? undefined : "0-1", + }; + + const expectedXinfoStreamFullResult = { + length: 1, + "radix-tree-keys": 1, + "radix-tree-nodes": 2, + "last-generated-id": "0-1", + entries: [["0-1", ["field1", "value1"]]], + groups: [], + "max-deleted-entry-id": versionLessThan7 ? undefined : "0-0", + "entries-added": versionLessThan7 ? undefined : 1, + "recorded-first-entry-id": versionLessThan7 ? undefined : "0-1", + }; + + if (result != null) { + expect(result[0]).toEqual("0-1"); // xadd + expect(result[1]).toEqual(expectedXinfoStreamResult); + expect(result[2]).toEqual(expectedXinfoStreamFullResult); + } + + client.close(); + }, + TIMEOUT, + ); + runBaseTests({ init: async (protocol, clientName?) => { const options = getClientConfigurationOption( diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c19c896000..69ed2c8ac2 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3894,13 +3894,7 @@ export function runBaseTests(config: { expect(await client.type(key)).toEqual("hash"); expect(await client.del([key])).toEqual(1); - await client.customCommand([ - "XADD", - key, - "*", - "field", - "value", - ]); + await client.xadd(key, [["field", "value"]]); expect(await client.type(key)).toEqual("stream"); expect(await client.del([key])).toEqual(1); expect(await client.type(key)).toEqual("none"); @@ -4753,6 +4747,224 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xinfo stream test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const groupName = `group-${uuidv4()}`; + const consumerName = `consumer-${uuidv4()}`; + const streamId0_0 = "0-0"; + const streamId1_0 = "1-0"; + const streamId1_1 = "1-1"; + + // Setup: add stream entry, create consumer group and consumer, read from stream with consumer + expect( + await client.xadd( + key, + [ + ["a", "b"], + ["c", "d"], + ], + { id: streamId1_0 }, + ), + ).toEqual(streamId1_0); + + // TODO: uncomment when XGROUP CREATE is implemented + // expect(await client.xgroupCreate(key, groupName, streamId0_0)).toEqual("Ok"); + expect( + await client.customCommand([ + "XGROUP", + "CREATE", + key, + groupName, + streamId0_0, + ]), + ).toEqual("OK"); + + // TODO: uncomment when XREADGROUP is implemented + // const xreadgroupResult = await client.xreadgroup([[key, ">"]], groupName, consumerName); + await client.customCommand([ + "XREADGROUP", + "GROUP", + groupName, + consumerName, + "STREAMS", + key, + ">", + ]); + + // test xinfoStream base (non-full) case: + const result = (await client.xinfoStream(key)) as { + length: number; + "radix-tree-keys": number; + "radix-tree-nodes": number; + "last-generated-id": string; + "max-deleted-entry-id": string; + "entries-added": number; + "recorded-first-entry-id": string; + "first-entry": (string | number | string[])[]; + "last-entry": (string | number | string[])[]; + groups: number; + }; + console.log(result); + + // verify result: + expect(result.length).toEqual(1); + const expectedFirstEntry = ["1-0", ["a", "b", "c", "d"]]; + expect(result["first-entry"]).toEqual(expectedFirstEntry); + expect(result["last-entry"]).toEqual(expectedFirstEntry); + expect(result.groups).toEqual(1); + + // Add one more entry + expect( + await client.xadd(key, [["foo", "bar"]], { + id: streamId1_1, + }), + ).toEqual(streamId1_1); + const fullResult = (await client.xinfoStream(key, 1)) as { + length: number; + "radix-tree-keys": number; + "radix-tree-nodes": number; + "last-generated-id": string; + "max-deleted-entry-id": string; + "entries-added": number; + "recorded-first-entry-id": string; + entries: (string | number | string[])[][]; + groups: [ + { + name: string; + "last-delivered-id": string; + "entries-read": number; + lag: number; + "pel-count": number; + pending: (string | number)[][]; + consumers: [ + { + name: string; + "seen-time": number; + "active-time": number; + "pel-count": number; + pending: (string | number)[][]; + }, + ]; + }, + ]; + }; + + // verify full result like: + // { + // length: 2, + // 'radix-tree-keys': 1, + // 'radix-tree-nodes': 2, + // 'last-generated-id': '1-1', + // 'max-deleted-entry-id': '0-0', + // 'entries-added': 2, + // 'recorded-first-entry-id': '1-0', + // entries: [ [ '1-0', ['a', 'b', ...] ] ], + // groups: [ { + // name: 'group', + // 'last-delivered-id': '1-0', + // 'entries-read': 1, + // lag: 1, + // 'pel-count': 1, + // pending: [ [ '1-0', 'consumer', 1722624726802, 1 ] ], + // consumers: [ { + // name: 'consumer', + // 'seen-time': 1722624726802, + // 'active-time': 1722624726802, + // 'pel-count': 1, + // pending: [ [ '1-0', 'consumer', 1722624726802, 1 ] ], + // } + // ] + // } + // ] + // } + expect(fullResult.length).toEqual(2); + expect(fullResult["recorded-first-entry-id"]).toEqual( + streamId1_0, + ); + + // Only the first entry will be returned since we passed count: 1 + expect(fullResult.entries).toEqual([expectedFirstEntry]); + + // compare groupName, consumerName, and pending messages from the full info result: + const fullResultGroups = fullResult.groups; + expect(fullResultGroups.length).toEqual(1); + expect(fullResultGroups[0]["name"]).toEqual(groupName); + + const pendingResult = fullResultGroups[0]["pending"]; + expect(pendingResult.length).toEqual(1); + expect(pendingResult[0][0]).toEqual(streamId1_0); + expect(pendingResult[0][1]).toEqual(consumerName); + + const consumersResult = fullResultGroups[0]["consumers"]; + expect(consumersResult.length).toEqual(1); + expect(consumersResult[0]["name"]).toEqual(consumerName); + + const consumerPendingResult = fullResultGroups[0]["pending"]; + expect(consumerPendingResult.length).toEqual(1); + expect(consumerPendingResult[0][0]).toEqual(streamId1_0); + expect(consumerPendingResult[0][1]).toEqual(consumerName); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xinfo stream edge cases and failures test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}-1-${uuidv4()}`; + const stringKey = `{key}-2-${uuidv4()}`; + const nonExistentKey = `{key}-3-${uuidv4()}`; + const streamId1_0 = "1-0"; + + // Setup: create empty stream + expect( + await client.xadd(key, [["field", "value"]], { + id: streamId1_0, + }), + ).toEqual(streamId1_0); + expect(await client.xdel(key, [streamId1_0])).toEqual(1); + + // XINFO STREAM called against empty stream + const result = await client.xinfoStream(key); + expect(result["length"]).toEqual(0); + expect(result["first-entry"]).toEqual(null); + expect(result["last-entry"]).toEqual(null); + + // XINFO STREAM FULL called against empty stream. Negative count values are ignored. + const fullResult = await client.xinfoStream(key, -3); + expect(fullResult["length"]).toEqual(0); + expect(fullResult["entries"]).toEqual([]); + expect(fullResult["groups"]).toEqual([]); + + // Calling XINFO STREAM with a non-existing key raises an error + await expect( + client.xinfoStream(nonExistentKey), + ).rejects.toThrow(); + await expect( + client.xinfoStream(nonExistentKey, true), + ).rejects.toThrow(); + await expect( + client.xinfoStream(nonExistentKey, 2), + ).rejects.toThrow(); + + // Key exists, but it is not a stream + await client.set(stringKey, "boofar"); + await expect(client.xinfoStream(stringKey)).rejects.toThrow(); + await expect( + client.xinfoStream(stringKey, true), + ).rejects.toThrow(); + await expect( + client.xinfoStream(stringKey, 2), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "rename test_%p", async (protocol) => { From 0425f56421fefa1794480d7f8bcbb90d77ee6ca9 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 16:55:31 -0700 Subject: [PATCH 157/236] Java: Fix `config_reset_stat` IT (#2099) Fix IT. Signed-off-by: Yury-Fridlyand --- java/integTest/src/test/java/glide/TestUtilities.java | 4 ++-- java/integTest/src/test/java/glide/cluster/CommandTests.java | 4 ++-- .../src/test/java/glide/standalone/CommandTests.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 55d5a69d55..9fc3a2931b 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -30,10 +30,10 @@ @UtilityClass public class TestUtilities { /** Extract integer parameter value from INFO command output */ - public static int getValueFromInfo(String data, String value) { + public static long getValueFromInfo(String data, String value) { for (var line : data.split("\r\n")) { if (line.contains(value)) { - return Integer.parseInt(line.split(":")[1]); + return Long.parseLong(line.split(":")[1]); } } fail(); diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 89ca922122..0ea506226e 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -416,14 +416,14 @@ public void clientGetName_with_multi_node_route() { public void config_reset_stat() { var data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); String firstNodeInfo = getFirstEntryFromMultiValue(data); - int value_before = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + long value_before = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); var result = clusterClient.configResetStat().get(); assertEquals(OK, result); data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); firstNodeInfo = getFirstEntryFromMultiValue(data); - int value_after = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + long value_after = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); assertTrue(value_after < value_before); } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 0a422e52dc..f518766b4c 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -278,13 +278,13 @@ public void clientGetName() { @SneakyThrows public void config_reset_stat() { String data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); - int value_before = getValueFromInfo(data, "total_net_input_bytes"); + long value_before = getValueFromInfo(data, "total_net_input_bytes"); var result = regularClient.configResetStat().get(); assertEquals(OK, result); data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); - int value_after = getValueFromInfo(data, "total_net_input_bytes"); + long value_after = getValueFromInfo(data, "total_net_input_bytes"); assertTrue(value_after < value_before); } From 60f71af24fdf6b797001b648ea1270e6fdaea083 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:36:08 -0700 Subject: [PATCH 158/236] Add Scala and Kotlin examples (#1933) * Add Scala and Kotlin examples Signed-off-by: Guian Gumpac --------- Signed-off-by: Jonathan Louie Signed-off-by: Guian Gumpac Co-authored-by: Yury-Fridlyand Co-authored-by: Guian Gumpac --- examples/.gitignore | 51 ++++ examples/java/README.md | 11 +- examples/kotlin/README.md | 22 ++ examples/kotlin/build.gradle.kts | 51 ++++ examples/kotlin/gradle.properties | 1 + .../kotlin/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/kotlin/gradlew | 234 ++++++++++++++++++ examples/kotlin/gradlew.bat | 89 +++++++ examples/kotlin/settings.gradle.kts | 5 + .../kotlin/src/main/kotlin/ClusterExample.kt | 142 +++++++++++ .../src/main/kotlin/StandaloneExample.kt | 127 ++++++++++ examples/node/.gitignore | 1 - examples/scala/README.md | 20 ++ examples/scala/build.sbt | 23 ++ examples/scala/project/build.properties | 1 + .../scala/src/main/scala/ClusterExample.scala | 138 +++++++++++ .../src/main/scala/StandaloneExample.scala | 127 ++++++++++ java/README.md | 19 ++ 19 files changed, 1065 insertions(+), 3 deletions(-) create mode 100644 examples/.gitignore create mode 100644 examples/kotlin/README.md create mode 100644 examples/kotlin/build.gradle.kts create mode 100644 examples/kotlin/gradle.properties create mode 100644 examples/kotlin/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/kotlin/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/kotlin/gradlew create mode 100644 examples/kotlin/gradlew.bat create mode 100644 examples/kotlin/settings.gradle.kts create mode 100644 examples/kotlin/src/main/kotlin/ClusterExample.kt create mode 100644 examples/kotlin/src/main/kotlin/StandaloneExample.kt delete mode 100644 examples/node/.gitignore create mode 100644 examples/scala/README.md create mode 100644 examples/scala/build.sbt create mode 100644 examples/scala/project/build.properties create mode 100644 examples/scala/src/main/scala/ClusterExample.scala create mode 100644 examples/scala/src/main/scala/StandaloneExample.scala diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000000..4c321f286b --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,51 @@ +### Kotlin ### +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### Scala ### +target +*.class + +### Node ### +*.js + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ +.bsp + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/examples/java/README.md b/examples/java/README.md index 395ca7d1a7..85d009df47 100644 --- a/examples/java/README.md +++ b/examples/java/README.md @@ -1,6 +1,5 @@ ## Run - -Ensure that you have an instance of Valkey running on "localhost" on "6379". Otherwise, update glide.examples.StandaloneExample or glide.examples.ClusterExample with a configuration that matches your server settings. +Ensure that you have a server running on "localhost" on port "6379". To run the ClusterExample, make sure that the server has cluster mode enabled. If the server is running on a different host and/or port, update the StandaloneExample or ClusterExample with a configuration that matches your server settings. To run the Standalone example: ``` @@ -12,3 +11,11 @@ To run the Cluster example: cd valkey-glide/examples/java ./gradlew :runCluster ``` + +## Version +These examples are running `valkey-glide` version `1.+`. In order to change the version, update the following section in the `build.gradle` file: +```groovy +dependencies { + implementation "io.valkey:valkey-glide:1.+:${osdetector.classifier}" +} +``` diff --git a/examples/kotlin/README.md b/examples/kotlin/README.md new file mode 100644 index 0000000000..a92035aeae --- /dev/null +++ b/examples/kotlin/README.md @@ -0,0 +1,22 @@ +## Run +Ensure that you have a server running on "localhost" on port "6379". To run the ClusterExample, make sure that the server has cluster mode enabled. If the server is running on a different host and/or port, update the StandaloneExample or ClusterExample with a configuration that matches your server settings. + +To run the Standalone example: +```shell +cd valkey-glide/examples/kotlin +./gradlew runStandalone +``` + +To run the Cluster example: +```shell +cd valkey-glide/examples/kotlin +./gradlew runCluster +``` + +## Version +These examples are running `valkey-glide` version `1.+`. In order to change the version, update the following section in the `build.gradle.kts` file: +```kotlin +dependencies { + implementation("io.valkey:valkey-glide:1.+:$classifier") +} +``` diff --git a/examples/kotlin/build.gradle.kts b/examples/kotlin/build.gradle.kts new file mode 100644 index 0000000000..37eee0a263 --- /dev/null +++ b/examples/kotlin/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + kotlin("jvm") version "1.9.23" + application + id("com.google.osdetector") version "1.7.3" +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +val os = osdetector.os +val classifier = osdetector.classifier +fun nettyTransport(): String { + if (os == "osx") + return "netty-transport-native-kqueue" + else if (os == "linux") + return "netty-transport-native-epoll" + else + throw Exception("Unsupported operating system $os") +} + +dependencies { + testImplementation(kotlin("test")) + implementation("io.valkey:valkey-glide:1.+:$classifier") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(11) +} + +tasks.register("runStandalone") { + group = "application" + description = "Run the standalone example" + classpath = sourceSets.main.get().runtimeClasspath + mainClass = "glide.examples.StandaloneExample" +} + +tasks.register("runCluster") { + group = "application" + description = "Run the cluster example" + classpath = sourceSets.main.get().runtimeClasspath + mainClass = "glide.examples.ClusterExample" +} diff --git a/examples/kotlin/gradle.properties b/examples/kotlin/gradle.properties new file mode 100644 index 0000000000..7fc6f1ff27 --- /dev/null +++ b/examples/kotlin/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/examples/kotlin/gradle/wrapper/gradle-wrapper.jar b/examples/kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/kotlin/gradlew.bat b/examples/kotlin/gradlew.bat new file mode 100644 index 0000000000..ac1b06f938 --- /dev/null +++ b/examples/kotlin/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/kotlin/settings.gradle.kts b/examples/kotlin/settings.gradle.kts new file mode 100644 index 0000000000..91009bbe9e --- /dev/null +++ b/examples/kotlin/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} +rootProject.name = "example" + diff --git a/examples/kotlin/src/main/kotlin/ClusterExample.kt b/examples/kotlin/src/main/kotlin/ClusterExample.kt new file mode 100644 index 0000000000..2a0d2c451c --- /dev/null +++ b/examples/kotlin/src/main/kotlin/ClusterExample.kt @@ -0,0 +1,142 @@ +package glide.examples + +import glide.api.GlideClusterClient +import glide.api.logging.Logger +import glide.api.models.commands.InfoOptions +import glide.api.models.configuration.GlideClusterClientConfiguration +import glide.api.models.configuration.NodeAddress +import glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES +import glide.api.models.exceptions.ClosingException +import glide.api.models.exceptions.ConnectionException +import glide.api.models.exceptions.ExecAbortException +import glide.api.models.exceptions.TimeoutException +import java.util.concurrent.CancellationException +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking + +object ClusterExample { + + /** + * Creates and returns a [GlideClusterClient] instance. + * + * This function initializes a [GlideClusterClient] with the provided list of nodes. + * The [nodesList] may contain the address of one or more cluster nodes, and the + * client will automatically discover all nodes in the cluster. + * + * @param nodesList A list of pairs where each pair + * contains a host (String) and port (Int). Defaults to [("localhost", 6379)]. + * + * @return An instance of [GlideClusterClient] connected to the discovered nodes. + */ + private suspend fun createClient(nodesList: List> = listOf(Pair("localhost", 6379))): GlideClusterClient { + // Check `GlideClusterClientConfiguration` for additional options. + val config = GlideClusterClientConfiguration.builder() + .addresses(nodesList.map({ (host: String, port: Int) -> NodeAddress.builder().host(host).port(port).build() })) + .clientName("test_cluster_client") + // Enable this field if the servers are configured with TLS. + //.useTLS(true) + .build() + + return GlideClusterClient.createClient(config).await() + } + + /** + * Executes the main logic of the application, performing basic operations + * such as SET, GET, PING, and INFO REPLICATION using the provided [GlideClusterClient]. + * + * @param client An instance of [GlideClusterClient]. + */ + private suspend fun appLogic(client: GlideClusterClient) { + // Send SET and GET + val setResponse = client.set("foo", "bar").await() + Logger.log(Logger.Level.INFO, "app", "Set response is $setResponse") + + val getResponse = client.get("foo").await() + Logger.log(Logger.Level.INFO, "app", "Get response is $getResponse") + + // Send PING to all primaries (according to Valkey's PING request_policy) + val pong = client.ping().await() + Logger.log(Logger.Level.INFO, "app", "Ping response is $pong") + + // Send INFO REPLICATION with routing option to all nodes + val infoReplResps = client.info( + InfoOptions.builder() + .section(InfoOptions.Section.REPLICATION) + .build(), + ALL_NODES + ).await() + Logger.log( + Logger.Level.INFO, + "app", + "INFO REPLICATION responses from all nodes are=\n$infoReplResps", + ) + } + + /** + * Executes the application logic with exception handling. + */ + private suspend fun execAppLogic() { + while (true) { + var client: GlideClusterClient? = null + try { + client = createClient() + return appLogic(client) + } catch (e: CancellationException) { + Logger.log(Logger.Level.ERROR, "glide", "Request cancelled: ${e.message}") + throw e + } catch (e: Exception) { + when (e) { + is ClosingException -> { + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + if (e.message?.contains("NOAUTH") == true) { + Logger.log(Logger.Level.ERROR, "glide", "Authentication error encountered: ${e.message}") + throw e + } else { + Logger.log(Logger.Level.WARN, "glide", "Client has closed and needs to be re-created: ${e.message}") + } + } + is TimeoutException -> { + // A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(Logger.Level.ERROR, "glide", "Timeout encountered: ${e.message}") + throw e + } + is ConnectionException -> { + // The client wasn't able to reestablish the connection within the given retries + Logger.log(Logger.Level.ERROR, "glide", "Connection error encountered: ${e.message}") + throw e + } + else -> { + Logger.log(Logger.Level.ERROR, "glide", "Execution error encountered: ${e.cause}") + throw e + } + } + } finally { + try { + client?.close() + } catch (e: Exception) { + Logger.log( + Logger.Level.WARN, + "glide", + "Encountered an error while closing the client: ${e.cause}" + ) + } + } + } + } + + /** + * The entry point of the cluster example. This method sets up the logger configuration + * and executes the main application logic. + * + * @param args Command-line arguments passed to the application. + */ + @JvmStatic + fun main(args: Array) = runBlocking { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(Logger.Level.INFO) + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + execAppLogic() + } +} diff --git a/examples/kotlin/src/main/kotlin/StandaloneExample.kt b/examples/kotlin/src/main/kotlin/StandaloneExample.kt new file mode 100644 index 0000000000..7ca809944c --- /dev/null +++ b/examples/kotlin/src/main/kotlin/StandaloneExample.kt @@ -0,0 +1,127 @@ +package glide.examples + +import glide.api.GlideClient +import glide.api.logging.Logger +import glide.api.models.configuration.GlideClientConfiguration +import glide.api.models.configuration.NodeAddress +import glide.api.models.exceptions.ClosingException +import glide.api.models.exceptions.ConnectionException +import glide.api.models.exceptions.ExecAbortException +import glide.api.models.exceptions.TimeoutException +import java.util.concurrent.CancellationException +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking + +object StandaloneExample { + + /** + * Creates and returns a [GlideClient] instance. + * + * This function initializes a [GlideClient] with the provided list of nodes. + * The [nodesList] may contain either only primary node or a mix of primary + * and replica nodes. The [GlideClient] use these nodes to connect to + * the Standalone setup servers. + * + * @param nodesList A list of pairs where each pair + * contains a host (String) and port (Int). Defaults to [("localhost", 6379)]. + * + * @return An instance of [GlideClient] connected to the specified nodes. + */ + private suspend fun createClient(nodesList: List> = listOf(Pair("localhost", 6379))): GlideClient { + // Check `GlideClientConfiguration` for additional options. + val config = GlideClientConfiguration.builder() + .addresses(nodesList.map({ (host: String, port: Int) -> NodeAddress.builder().host(host).port(port).build() })) + // Enable this field if the servers are configured with TLS. + //.useTLS(true) + .build() + + return GlideClient.createClient(config).await() + } + + /** + * Executes the main logic of the application, performing basic operations such as SET, GET, and + * PING using the provided [GlideClient]. + * + * @param client An instance of [GlideClient]. + */ + private suspend fun appLogic(client: GlideClient) { + // Send SET and GET + val setResponse = client.set("foo", "bar").await() + Logger.log(Logger.Level.INFO, "app", "Set response is $setResponse") + + val getResponse = client.get("foo").await() + Logger.log(Logger.Level.INFO, "app", "Get response is $getResponse") + + // Send PING to the primary node + val pong = client.ping().await() + Logger.log(Logger.Level.INFO, "app", "Ping response is $pong") + } + + /** + * Executes the application logic with exception handling. + */ + private suspend fun execAppLogic() { + while (true) { + var client: GlideClient? = null + try { + client = createClient() + return appLogic(client) + } catch (e: CancellationException) { + Logger.log(Logger.Level.ERROR, "glide", "Request cancelled: ${e.message}") + throw e + } catch (e: Exception) { + when (e) { + is ClosingException -> { + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + if (e.message?.contains("NOAUTH") == true) { + Logger.log(Logger.Level.ERROR, "glide", "Authentication error encountered: ${e.message}") + throw e + } else { + Logger.log(Logger.Level.WARN, "glide", "Client has closed and needs to be re-created: ${e.message}") + } + } + is TimeoutException -> { + // A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(Logger.Level.ERROR, "glide", "Timeout encountered: ${e.message}") + throw e + } + is ConnectionException -> { + // The client wasn't able to reestablish the connection within the given retries + Logger.log(Logger.Level.ERROR, "glide", "Connection error encountered: ${e.message}") + throw e + } + else -> { + Logger.log(Logger.Level.ERROR, "glide", "Execution error encountered: ${e.cause}") + throw e + } + } + } finally { + try { + client?.close() + } catch (e: Exception) { + Logger.log( + Logger.Level.WARN, + "glide", + "Encountered an error while closing the client: ${e.cause}" + ) + } + } + } + } + + /** + * The entry point of the standalone example. This method sets up the logger configuration + * and executes the main application logic. + * + * @param args Command-line arguments passed to the application. + */ + @JvmStatic + fun main(args: Array) = runBlocking { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(Logger.Level.INFO) + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + execAppLogic() + } +} diff --git a/examples/node/.gitignore b/examples/node/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/examples/node/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/examples/scala/README.md b/examples/scala/README.md new file mode 100644 index 0000000000..fdbfa6b397 --- /dev/null +++ b/examples/scala/README.md @@ -0,0 +1,20 @@ +## Run +Ensure that you have a server running on "localhost" on port "6379". To run the ClusterExample, make sure that the server has cluster mode enabled. If the server is running on a different host and/or port, update the StandaloneExample or ClusterExample with a configuration that matches your server settings. + +To run the Standalone example: +```shell +cd valkey-glide/examples/scala +sbt "runMain StandaloneExample" +``` + +To run the Cluster example: +```shell +cd valkey-glide/examples/scala +sbt "runMain ClusterExample" +``` + +## Version +These examples are running `valkey-glide` version `1.+`. In order to change the version, update the following section in the `build.sbt` file: +```scala +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier platformClassifier +``` diff --git a/examples/scala/build.sbt b/examples/scala/build.sbt new file mode 100644 index 0000000000..811d0e9840 --- /dev/null +++ b/examples/scala/build.sbt @@ -0,0 +1,23 @@ +ThisBuild / version := "0.1.0-SNAPSHOT" + +ThisBuild / scalaVersion := "3.3.3" + +lazy val root = (project in file(".")) + .settings( + name := "example" + ) + +// TODO: Get classifier using https://github.com/phdata/sbt-os-detector if/when https://repository.phdata.io/artifactory/libs-release works again +val os = System.getProperty("os.name").toLowerCase +val platformClassifier = { + (os, System.getProperty("os.arch").toLowerCase) match { + case (mac, arm) if mac.contains("mac") && (arm.contains("aarch") || arm.contains("arm")) => "osx-aarch_64" + case (mac, x86) if mac.contains("mac") && x86.contains("x86") => "osx-x86_64" + case (linux, arm) if linux.contains("linux") && (arm.contains("aarch") || arm.contains("arm")) => "linux-aarch_64" + case (linux, x86) if linux.contains("linux") && x86.contains("x86") => "linux-x86_64" + case (osName, archName) => throw new RuntimeException(s"Unsupported platform $osName $archName") + } +} + +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier platformClassifier + diff --git a/examples/scala/project/build.properties b/examples/scala/project/build.properties new file mode 100644 index 0000000000..136f452e0d --- /dev/null +++ b/examples/scala/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.10.1 diff --git a/examples/scala/src/main/scala/ClusterExample.scala b/examples/scala/src/main/scala/ClusterExample.scala new file mode 100644 index 0000000000..b95b557f9c --- /dev/null +++ b/examples/scala/src/main/scala/ClusterExample.scala @@ -0,0 +1,138 @@ +import glide.api.GlideClusterClient +import glide.api.logging.Logger +import glide.api.models.commands.InfoOptions +import glide.api.models.configuration.{GlideClusterClientConfiguration, NodeAddress} +import glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES +import glide.api.models.exceptions.{ClosingException, ConnectionException, ExecAbortException, TimeoutException} + +import scala.concurrent.{Await, CancellationException, ExecutionContext, Future} +import scala.concurrent.duration.Duration +import scala.concurrent.ExecutionContext.Implicits.global +import scala.jdk.CollectionConverters.* +import scala.jdk.FutureConverters.* +import scala.util.{Failure, Try} + +object ClusterExample { + + /** + * Creates and returns a GlideClusterClient instance. + * + * This function initializes a GlideClusterClient with the provided list of nodes. + * The nodesList may contain the address of one or more cluster nodes, and the + * client will automatically discover all nodes in the cluster. + * + * @param nodesList A list of pairs where each pair + * contains a host (String) and port (Int). Defaults to [("localhost", 6379)]. + * + * @return An instance of Future[GlideClusterClient] connected to the specified nodes. + */ + private def createClient(nodesList: List[(String, Int)] = List(("localhost", 6379))): Future[GlideClusterClient] = { + // Check `GlideClientConfiguration` for additional options. + val config = GlideClusterClientConfiguration.builder() + .addresses(nodesList.map((host, port) => NodeAddress.builder().host(host).port(port).build()).asJava) + // Enable this field if the servers are configured with TLS. + //.useTLS(true) + .build() + // This cast is required in order to pass the config to createClient because the Scala type system + // is unable to resolve the Lombok builder result type. + .asInstanceOf[GlideClusterClientConfiguration] + + GlideClusterClient.createClient(config).asScala + } + + /** + * Executes the main logic of the application, performing basic operations + * such as SET, GET, PING, and INFO REPLICATION using the provided GlideClusterClient. + * + * @param client An instance of GlideClusterClient. + */ + private def appLogic(client: GlideClusterClient): Future[Unit] = { + for { + // Send SET and GET + setResponse <- client.set("foo", "bar").asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Set response is $setResponse") + + getResponse <- client.get("foo").asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Get response is $getResponse") + + // Send PING to all primaries (according to Valkey's PING request_policy) + pong <- client.ping().asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Ping response is $pong") + + // Send INFO REPLICATION with routing option to all nodes + infoReplResps <- client.info( + InfoOptions.builder() + .section(InfoOptions.Section.REPLICATION) + .build(), + ALL_NODES + ).asScala + _ = Logger.log( + Logger.Level.INFO, + "app", + "INFO REPLICATION responses from all nodes are=\n$infoReplResps", + ) + } yield () + } + + /** + * Executes the application logic with exception handling. + */ + private def execAppLogic(): Future[Unit] = { + def loop(): Future[Unit] = { + createClient().flatMap(client => appLogic(client).andThen { + case _ => Try(client.close()) match { + case Failure(e) => + Logger.log( + Logger.Level.WARN, + "glide", + s"Encountered an error while closing the client: ${e.getCause}" + ) + case _ => () + } + }).recoverWith { + case e: CancellationException => + Logger.log(Logger.Level.ERROR, "glide", s"Request cancelled: ${e.getMessage}") + Future.failed(e) + case e: Exception => e.getCause match { + case e: ClosingException if e.getMessage.contains("NOAUTH") => + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + Logger.log(Logger.Level.ERROR, "glide", s"Authentication error encountered: ${e.getMessage}") + Future.failed(e) + case e: ClosingException => + Logger.log(Logger.Level.WARN, "glide", s"Client has closed and needs to be re-created: ${e.getMessage}") + loop() + case e: TimeoutException => + // A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(Logger.Level.ERROR, "glide", s"Timeout encountered: ${e.getMessage}") + Future.failed(e) + case e: ConnectionException => + // The client wasn't able to reestablish the connection within the given retries + Logger.log(Logger.Level.ERROR, "glide", s"Connection error encountered: ${e.getMessage}") + Future.failed(e) + case _ => + Logger.log(Logger.Level.ERROR, "glide", s"Execution error encountered: ${e.getCause}") + Future.failed(e) + } + } + } + + loop() + } + + /** + * The entry point of the standalone example. This method sets up the logger configuration + * and executes the main application logic. + * + * @param args Command-line arguments passed to the application. + */ + def main(args: Array[String]): Unit = { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(Logger.Level.INFO) + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + + // Await is used only for this example. Not recommended for use in production environments. + Await.result(execAppLogic(), Duration.Inf) + } +} diff --git a/examples/scala/src/main/scala/StandaloneExample.scala b/examples/scala/src/main/scala/StandaloneExample.scala new file mode 100644 index 0000000000..401a4520c7 --- /dev/null +++ b/examples/scala/src/main/scala/StandaloneExample.scala @@ -0,0 +1,127 @@ +import glide.api.GlideClient +import glide.api.logging.Logger +import glide.api.models.configuration.GlideClientConfiguration +import glide.api.models.configuration.NodeAddress +import glide.api.models.exceptions.{ClosingException, ConnectionException, ExecAbortException, TimeoutException} + +import java.util.concurrent.TimeUnit +import scala.concurrent.{Await, CancellationException, ExecutionContext, Future} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.Duration +import scala.jdk.CollectionConverters.* +import scala.jdk.FutureConverters.* +import scala.util.{Failure, Try} + +object StandaloneExample { + + /** + * Creates and returns a GlideClient instance. + * + * This function initializes a GlideClient with the provided list of nodes. + * The nodesLis may contain either only primary node or a mix of primary + * and replica nodes. The GlideClient use these nodes to connect to + * the Standalone setup servers. + * + * @param nodesList A list of pairs where each pair + * contains a host (String) and port (Int). Defaults to [("localhost", 6379)]. + * + * @return An instance of Future[GlideClient] connected to the specified nodes. + */ + private def createClient(nodesList: List[(String, Int)] = List(("localhost", 6379))): Future[GlideClient] = { + // Check `GlideClientConfiguration` for additional options. + val config = GlideClientConfiguration.builder() + .addresses(nodesList.map((host, port) => NodeAddress.builder().host(host).port(port).build()).asJava) + // Enable this field if the servers are configured with TLS. + //.useTLS(true) + .build() + // This cast is required in order to pass the config to createClient because the Scala type system + // is unable to resolve the Lombok builder result type. + .asInstanceOf[GlideClientConfiguration] + + GlideClient.createClient(config).asScala + } + + /** + * Executes the main logic of the application, performing basic operations such as SET, GET, and + * PING using the provided GlideClient. + * + * @param client An instance of GlideClient. + */ + private def appLogic(client: GlideClient): Future[Unit] = { + for { + // Send SET and GET + setResponse <- client.set("foo", "bar").asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Set response is $setResponse") + + getResponse <- client.get("foo").asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Get response is $getResponse") + + // Send PING to the primary node + pong <- client.ping().asScala + _ = Logger.log(Logger.Level.INFO, "app", s"Ping response is $pong") + } yield () + } + + /** + * Executes the application logic with exception handling. + */ + private def execAppLogic(): Future[Unit] = { + def loop(): Future[Unit] = { + createClient().flatMap(client => appLogic(client).andThen { + case _ => Try(client.close()) match { + case Failure(e) => + Logger.log( + Logger.Level.WARN, + "glide", + s"Encountered an error while closing the client: ${e.getCause}" + ) + case _ => () + } + }).recoverWith { + case e: CancellationException => + Logger.log(Logger.Level.ERROR, "glide", s"Request cancelled: ${e.getMessage}") + Future.failed(e) + case e: Exception => e.getCause match { + case e: ClosingException if e.getMessage.contains("NOAUTH") => + // If the error message contains "NOAUTH", raise the exception + // because it indicates a critical authentication issue. + Logger.log(Logger.Level.ERROR, "glide", s"Authentication error encountered: ${e.getMessage}") + Future.failed(e) + case e: ClosingException => + Logger.log(Logger.Level.WARN, "glide", s"Client has closed and needs to be re-created: ${e.getMessage}") + loop() + case e: TimeoutException => + // A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(Logger.Level.ERROR, "glide", s"Timeout encountered: ${e.getMessage}") + Future.failed(e) + case e: ConnectionException => + // The client wasn't able to reestablish the connection within the given retries + Logger.log(Logger.Level.ERROR, "glide", s"Connection error encountered: ${e.getMessage}") + Future.failed(e) + case _ => + Logger.log(Logger.Level.ERROR, "glide", s"Execution error encountered: ${e.getCause}") + Future.failed(e) + } + } + } + + loop() + } + + /** + * The entry point of the standalone example. This method sets up the logger configuration + * and executes the main application logic. + * + * @param args Command-line arguments passed to the application. + */ + def main(args: Array[String]): Unit = { + // In this example, we will utilize the client's logger for all log messages + Logger.setLoggerConfig(Logger.Level.INFO) + // Optional - set the logger to write to a file + // Logger.setLoggerConfig(Logger.Level.INFO, file) + + val Timeout = 50 + // Change the timeout based on your production environments. + Await.result(execAppLogic(), Duration(Timeout, TimeUnit.SECONDS)) + } +} diff --git a/java/README.md b/java/README.md index d31048b22a..3c1a8cbe1d 100644 --- a/java/README.md +++ b/java/README.md @@ -130,6 +130,22 @@ Maven: ``` +SBT: +- **IMPORTANT** must include a `classifier`. Please use this dependency block and add it to the build.sbt file. +```scala +// osx-aarch_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "osx-aarch_64" + +// osx-x86_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "osx-x86_64" + +// linux-aarch_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux-aarch_64" + +// linux-x86_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux-x86_64" +``` + ## Setting up the Java module To use Valkey GLIDE in a Java project with modules, include a module-info.java in your project. @@ -245,6 +261,9 @@ public class Main { } ``` +### Scala and Kotlin Examples +See [our Scala and Kotlin examples](../examples/) to learn how to use Valkey GLIDE in Scala and Kotlin projects. + ### Accessing tests For more examples, you can refer to the test folder [unit tests](./client/src/test/java/glide/api) and [integration tests](./integTest/src/test/java/glide). From 9277a2f6cc3a69f23b629e312c055f1aaf02ff8c Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Thu, 8 Aug 2024 13:24:37 -0700 Subject: [PATCH 159/236] Node: Add `XGROUP CREATECONSUMER` and `XGROUP DELCONSUMER` commands (#2088) * Added XGROUP CREATE and XGROUP DESTROY commands Signed-off-by: Guian Gumpac --------- Signed-off-by: Guian Gumpac --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 54 +++++++++++++++ node/src/Commands.ts | 30 ++++++++ node/src/Transaction.ts | 44 ++++++++++++ node/tests/SharedTests.ts | 133 ++++++++++++++++++++++++++++++------ node/tests/TestUtilities.ts | 18 ++--- 6 files changed, 252 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7bde5852..fa7f24b6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ * Python: Added PUBSUB * commands ([#2043](https://github.com/valkey-io/valkey-glide/pull/2043)) * Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) * Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) +* Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4096424b2e..e93bcd2c2d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -165,6 +165,8 @@ import { createXGroupDestroy, createXInfoConsumers, createXInfoStream, + createXGroupCreateConsumer, + createXGroupDelConsumer, createXLen, createXRead, createXTrim, @@ -4098,6 +4100,58 @@ export class BaseClient { ); } + /** + * Creates a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-createconsumer for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param consumerName - The newly created consumer. + * @returns `true` if the consumer is created. Otherwise, returns `false`. + * + * @example + * ```typescript + * // The consumer "myconsumer" was created in consumer group "mygroup" for the stream "mystream". + * console.log(await client.xgroupCreateConsumer("mystream", "mygroup", "myconsumer")); // Output is true + * ``` + */ + public async xgroupCreateConsumer( + key: string, + groupName: string, + consumerName: string, + ): Promise { + return this.createWritePromise( + createXGroupCreateConsumer(key, groupName, consumerName), + ); + } + + /** + * Deletes a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-delconsumer for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param consumerName - The consumer to delete. + * @returns The number of pending messages the `consumer` had before it was deleted. + * + * * @example + * ```typescript + * // Consumer "myconsumer" was deleted, and had 5 pending messages unclaimed. + * console.log(await client.xgroupDelConsumer("mystream", "mygroup", "myconsumer")); // Output is 5 + * ``` + */ + public async xgroupDelConsumer( + key: string, + groupName: string, + consumerName: string, + ): Promise { + return this.createWritePromise( + createXGroupDelConsumer(key, groupName, consumerName), + ); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d20e029e67..6f9715ddf4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2027,6 +2027,36 @@ export function createXTrim( return createCommand(RequestType.XTrim, args); } +/** + * @internal + */ +export function createXGroupCreateConsumer( + key: string, + groupName: string, + consumerName: string, +): command_request.Command { + return createCommand(RequestType.XGroupCreateConsumer, [ + key, + groupName, + consumerName, + ]); +} + +/** + * @internal + */ +export function createXGroupDelConsumer( + key: string, + groupName: string, + consumerName: string, +): command_request.Command { + return createCommand(RequestType.XGroupDelConsumer, [ + key, + groupName, + consumerName, + ]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 4d7f8d81cb..937909f15b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -202,6 +202,8 @@ import { createXTrim, createXGroupCreate, createXGroupDestroy, + createXGroupCreateConsumer, + createXGroupDelConsumer, createZAdd, createZCard, createZCount, @@ -2386,6 +2388,48 @@ export class BaseTransaction> { return this.addAndReturn(createXGroupDestroy(key, groupName)); } + /** + * Creates a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-createconsumer for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param consumerName - The newly created consumer. + * + * Command Response - `true` if the consumer is created. Otherwise, returns `false`. + */ + public xgroupCreateConsumer( + key: string, + groupName: string, + consumerName: string, + ): T { + return this.addAndReturn( + createXGroupCreateConsumer(key, groupName, consumerName), + ); + } + + /** + * Deletes a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. + * + * See https://valkey.io/commands/xgroup-delconsumer for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param consumerName - The consumer to delete. + * + * Command Response - The number of pending messages the `consumer` had before it was deleted. + */ + public xgroupDelConsumer( + key: string, + groupName: string, + consumerName: string, + ): T { + return this.addAndReturn( + createXGroupDelConsumer(key, groupName, consumerName), + ); + } + /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 69ed2c8ac2..db27f80007 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4770,16 +4770,8 @@ export function runBaseTests(config: { ), ).toEqual(streamId1_0); - // TODO: uncomment when XGROUP CREATE is implemented - // expect(await client.xgroupCreate(key, groupName, streamId0_0)).toEqual("Ok"); expect( - await client.customCommand([ - "XGROUP", - "CREATE", - key, - groupName, - streamId0_0, - ]), + await client.xgroupCreate(key, groupName, streamId0_0), ).toEqual("OK"); // TODO: uncomment when XREADGROUP is implemented @@ -7373,13 +7365,11 @@ export function runBaseTests(config: { } expect( - await client.customCommand([ - "XGROUP", - "CREATECONSUMER", + await client.xgroupCreateConsumer( key, groupName1, consumer2, - ]), + ), ).toBeTruthy(); expect( await client.customCommand([ @@ -7452,13 +7442,7 @@ export function runBaseTests(config: { }), ).toEqual("OK"); expect( - await client.customCommand([ - "xgroup", - "createconsumer", - key, - group, - "consumer", - ]), + await client.xgroupCreateConsumer(key, group, "consumer"), ).toEqual(true); expect( @@ -7671,6 +7655,115 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xgroupCreateConsumer and xgroupDelConsumer test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonExistentKey = uuidv4(); + const stringKey = uuidv4(); + const groupName = uuidv4(); + const consumer = uuidv4(); + const streamId0 = "0"; + + // create group and consumer for the group + expect( + await client.xgroupCreate(key, groupName, streamId0, { + mkStream: true, + }), + ).toEqual("OK"); + expect( + await client.xgroupCreateConsumer(key, groupName, consumer), + ).toEqual(true); + + // attempting to create/delete a consumer for a group that does not exist results in a NOGROUP request error + await expect( + client.xgroupCreateConsumer( + key, + "nonExistentGroup", + consumer, + ), + ).rejects.toThrow(RequestError); + await expect( + client.xgroupDelConsumer(key, "nonExistentGroup", consumer), + ).rejects.toThrow(RequestError); + + // attempt to create consumer for group again + expect( + await client.xgroupCreateConsumer(key, groupName, consumer), + ).toEqual(false); + + // attempting to delete a consumer that has not been created yet returns 0 + expect( + await client.xgroupDelConsumer( + key, + groupName, + "nonExistentConsumer", + ), + ).toEqual(0); + + // Add two stream entries + const streamid1: string | null = await client.xadd(key, [ + ["field1", "value1"], + ]); + expect(streamid1).not.toBeNull(); + const streamid2 = await client.xadd(key, [ + ["field2", "value2"], + ]); + expect(streamid2).not.toBeNull(); + + // read the entire stream for the consumer and mark messages as pending + expect( + await client.customCommand([ + "XREADGROUP", + "GROUP", + groupName, + consumer, + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + [streamid1 as string]: [["field1", "value1"]], + [streamid2 as string]: [["field2", "value2"]], + }, + }); + + // delete one of the streams + expect( + await client.xgroupDelConsumer(key, groupName, consumer), + ).toEqual(2); + + // attempting to call XGROUP CREATECONSUMER or XGROUP DELCONSUMER with a non-existing key should raise an error + await expect( + client.xgroupCreateConsumer( + nonExistentKey, + groupName, + consumer, + ), + ).rejects.toThrow(RequestError); + await expect( + client.xgroupDelConsumer( + nonExistentKey, + groupName, + consumer, + ), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xgroupCreateConsumer(stringKey, groupName, consumer), + ).rejects.toThrow(RequestError); + await expect( + client.xgroupDelConsumer(stringKey, groupName, consumer), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `xgroupCreate and xgroupDestroy test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 24d4611df2..ef044bd008 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -515,6 +515,7 @@ export async function transactionTest( const value = uuidv4(); const groupName1 = uuidv4(); const groupName2 = uuidv4(); + const consumer = uuidv4(); // array of tuples - first element is test name/description, second - expected return value const responseData: [string, ReturnType][] = []; @@ -959,15 +960,9 @@ export async function transactionTest( // key9 has one entry here: {"0-2":[["field","value2"]]} - baseTransaction.customCommand([ - "xgroup", - "createconsumer", - key9, - groupName1, - "consumer1", - ]); + baseTransaction.xgroupCreateConsumer(key9, groupName1, "consumer1"); responseData.push([ - 'xgroupCreateConsumer(key9, groupName1, "consumer1")', + "xgroupCreateConsumer(key9, groupName1, consumer1)", true, ]); baseTransaction.customCommand([ @@ -1011,6 +1006,13 @@ export async function transactionTest( 'xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', ["0-2"], ]); + baseTransaction.xgroupCreateConsumer(key9, groupName1, consumer); + responseData.push([ + "xgroupCreateConsumer(key9, groupName1, consumer)", + true, + ]); + baseTransaction.xgroupDelConsumer(key9, groupName1, consumer); + responseData.push(["xgroupDelConsumer(key9, groupName1, consumer)", 0]); baseTransaction.xgroupDestroy(key9, groupName1); responseData.push(["xgroupDestroy(key9, groupName1)", true]); baseTransaction.xgroupDestroy(key9, groupName2); From c101a6d92a5bec09efee0eb6f80fecfad642abc7 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Fri, 9 Aug 2024 09:50:49 -0700 Subject: [PATCH 160/236] Node: Add `ZRANGESTORE` command (#2068) * Node: Add ZRANGESTORE command --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 68 ++++++- node/src/Commands.ts | 16 ++ node/src/Transaction.ts | 59 ++++-- node/tests/GlideClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 259 +++++++++++++++++++++++++- node/tests/TestUtilities.ts | 5 + 7 files changed, 380 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7f24b6d7..83751c2ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZRANGESTORE command ([#2068](https://github.com/valkey-io/valkey-glide/pull/2068)) * Node: Added SRANDMEMBER command ([#2067](https://github.com/valkey-io/valkey-glide/pull/2067)) * Node: Added XINFO STREAM command ([#2083](https://github.com/valkey-io/valkey-glide/pull/2083)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e93bcd2c2d..f2bbe54e4a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -46,6 +46,7 @@ import { SearchOrigin, SetOptions, StreamAddOptions, + StreamClaimOptions, StreamGroupOptions, StreamReadOptions, StreamTrimOptions, @@ -160,6 +161,7 @@ import { createUnlink, createWatch, createXAdd, + createXClaim, createXDel, createXGroupCreate, createXGroupDestroy, @@ -186,6 +188,7 @@ import { createZPopMin, createZRandMember, createZRange, + createZRangeStore, createZRangeWithScores, createZRank, createZRem, @@ -196,8 +199,6 @@ import { createZRevRankWithScore, createZScan, createZScore, - StreamClaimOptions, - createXClaim, } from "./Commands"; import { ClosingError, @@ -3139,10 +3140,10 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @returns A list of elements within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. * @@ -3177,10 +3178,10 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @returns A map of elements and their scores within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. * @@ -3215,6 +3216,53 @@ export class BaseClient { ); } + /** + * Stores a specified range of elements from the sorted set at `source`, into a new + * sorted set at `destination`. If `destination` doesn't exist, a new sorted + * set is created; if it exists, it's overwritten. + * + * See https://valkey.io/commands/zrangestore/ for more details. + * + * @remarks When in cluster mode, `destination` and `source` must map to the same hash slot. + * @param destination - The key for the destination sorted set. + * @param source - The key of the source sorted set. + * @param rangeQuery - The range query object representing the type of range query to perform. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. + * @returns The number of elements in the resulting sorted set. + * + * since - Redis version 6.2.0. + * + * @example + * ```typescript + * // Example usage of zrangeStore to retrieve and store all members of a sorted set in ascending order. + * const result = await client.zrangeStore("destination_key", "my_sorted_set", { start: 0, stop: -1 }); + * console.log(result); // Output: 7 - "destination_key" contains a sorted set with the 7 members from "my_sorted_set". + * ``` + * @example + * ```typescript + * // Example usage of zrangeStore method to retrieve members within a score range in ascending order and store in "destination_key" + * const result = await client.zrangeStore("destination_key", "my_sorted_set", { + * start: InfScoreBoundary.NegativeInfinity, + * stop: { value: 3, isInclusive: false }, + * type: "byScore", + * }); + * console.log(result); // Output: 5 - Stores 5 members with scores within the range of negative infinity to 3, in ascending order, in "destination_key". + * ``` + */ + public zrangeStore( + destination: string, + source: string, + rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + reverse: boolean = false, + ): Promise { + return this.createWritePromise( + createZRangeStore(destination, source, rangeQuery, reverse), + ); + } + /** * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 6f9715ddf4..d13c047461 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1737,6 +1737,22 @@ export function createZRangeWithScores( return createCommand(RequestType.ZRange, args); } +/** + * @internal + */ +export function createZRangeStore( + destination: string, + source: string, + rangeQuery: RangeByIndex | RangeByScore | RangeByLex, + reverse: boolean = false, +): command_request.Command { + const args = [ + destination, + ...createZRangeArgs(source, rangeQuery, reverse, false), + ]; + return createCommand(RequestType.ZRangeStore, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 937909f15b..0004c600a8 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -31,6 +31,7 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, + GeoSearchStoreResultOptions, GeoUnit, GeospatialData, InfoOptions, @@ -99,6 +100,7 @@ import { createGeoHash, createGeoPos, createGeoSearch, + createGeoSearchStore, createGet, createGetBit, createGetDel, @@ -195,15 +197,15 @@ import { createXAdd, createXClaim, createXDel, + createXGroupCreate, + createXGroupCreateConsumer, + createXGroupDelConsumer, + createXGroupDestroy, createXInfoConsumers, createXInfoStream, createXLen, createXRead, createXTrim, - createXGroupCreate, - createXGroupDestroy, - createXGroupCreateConsumer, - createXGroupDelConsumer, createZAdd, createZCard, createZCount, @@ -220,6 +222,7 @@ import { createZPopMin, createZRandMember, createZRange, + createZRangeStore, createZRangeWithScores, createZRank, createZRem, @@ -230,8 +233,6 @@ import { createZRevRankWithScore, createZScan, createZScore, - createGeoSearchStore, - GeoSearchStoreResultOptions, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1743,10 +1744,10 @@ export class BaseTransaction> { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * * Command Response - A list of elements within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. @@ -1765,10 +1766,10 @@ export class BaseTransaction> { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * * Command Response - A map of elements and their scores within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. @@ -1783,6 +1784,36 @@ export class BaseTransaction> { ); } + /** + * Stores a specified range of elements from the sorted set at `source`, into a new + * sorted set at `destination`. If `destination` doesn't exist, a new sorted + * set is created; if it exists, it's overwritten. + * + * See https://valkey.io/commands/zrangestore/ for more details. + * + * @param destination - The key for the destination sorted set. + * @param source - The key of the source sorted set. + * @param rangeQuery - The range query object representing the type of range query to perform. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. + * + * Command Response - The number of elements in the resulting sorted set. + * + * since - Redis version 6.2.0. + */ + public zrangeStore( + destination: string, + source: string, + rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + reverse: boolean = false, + ): T { + return this.addAndReturn( + createZRangeStore(destination, source, rangeQuery, reverse), + ); + } + /** * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 7165837804..db4b9f3282 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -368,6 +368,7 @@ describe("GlideClusterClient", () => { { member: "_" }, { radius: 5, unit: GeoUnit.METERS }, ), + client.zrangeStore("abc", "zyx", { start: 0, stop: -1 }), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index db27f80007..1bfdbada9d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3702,24 +3702,257 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `zrange different typesn of keys test_%p`, + `zrangeStore by index test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - const key = uuidv4(); + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + expect( - await client.zrange("nonExistingKey", { + await client.zrangeStore(destkey, key, { + start: 0, + stop: 1, + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["one", "two"]); + + expect( + await client.zrangeStore( + destkey, + key, + { start: 0, stop: 1 }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["three", "two"]); + + expect( + await client.zrangeStore(destkey, key, { + start: 3, + stop: 1, + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrangeStore by score test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["one", "two"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: { value: 3, isInclusive: false }, + stop: InfScoreBoundary.NegativeInfinity, + type: "byScore", + }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["two", "one"]); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, + limit: { offset: 1, count: 2 }, + type: "byScore", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["two", "three"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }, + true, + ), + ).toEqual(0); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.PositiveInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrangeStore by lex test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { a: 1, b: 2, c: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["a", "b"]); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, + limit: { offset: 1, count: 2 }, + type: "byLex", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["b", "c"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: { value: "c", isInclusive: false }, + stop: InfScoreBoundary.NegativeInfinity, + type: "byLex", + }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["b", "a"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }, + true, + ), + ).toEqual(0); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.PositiveInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrange and zrangeStore different types of keys test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + const key = "{testKey}:1-" + uuidv4(); + const nonExistingKey = "{testKey}:2-" + uuidv4(); + const destkey = "{testKey}:3-" + uuidv4(); + + // test non-existing key - return an empty set + expect( + await client.zrange(nonExistingKey, { start: 0, stop: 1, }), ).toEqual([]); expect( - await client.zrangeWithScores("nonExistingKey", { + await client.zrangeWithScores(nonExistingKey, { start: 0, stop: 1, }), ).toEqual({}); + // test against a non-sorted set - throw RequestError expect(await client.set(key, "value")).toEqual("OK"); await expect( @@ -3729,6 +3962,22 @@ export function runBaseTests(config: { await expect( client.zrangeWithScores(key, { start: 0, stop: 1 }), ).rejects.toThrow(); + + // test zrangeStore - added in version 6.2.0 + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + // test non-existing key - stores an empty set + expect( + await client.zrangeStore(destkey, nonExistingKey, { + start: 0, + stop: 1, + }), + ).toEqual(0); + + // test against a non-sorted set - throw RequestError + await expect( + client.zrangeStore(destkey, key, { start: 0, stop: 1 }), + ).rejects.toThrow(); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ef044bd008..ec2090f520 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -825,6 +825,11 @@ export async function transactionTest( responseData.push(["zadd(key13, { one: 1, two: 2, three: 3.5 })", 3]); if (gte(version, "6.2.0")) { + baseTransaction.zrangeStore(key8, key8, { start: 0, stop: -1 }); + responseData.push([ + "zrangeStore(key8, key8, { start: 0, stop: -1 })", + 4, + ]); baseTransaction.zdiff([key13, key12]); responseData.push(["zdiff([key13, key12])", ["three"]]); baseTransaction.zdiffWithScores([key13, key12]); From 329f7e672966baae22aa12fec9584ac80d947456 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Fri, 9 Aug 2024 10:04:57 -0700 Subject: [PATCH 161/236] Node: added MOVE command (#2104) * Node: added MOVE command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/Commands.ts | 10 +++++++++ node/src/GlideClient.ts | 21 ++++++++++++++++++ node/src/Transaction.ts | 16 ++++++++++++++ node/tests/GlideClient.test.ts | 40 ++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83751c2ea5..7d8b35ef31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) * Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) * Node: Added COPY command ([#2024](https://github.com/valkey-io/valkey-glide/pull/2024)) +* Node: Added MOVE command ([#2104](https://github.com/valkey-io/valkey-glide/pull/2104)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d13c047461..489eb198a1 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2697,6 +2697,16 @@ export function createCopy( return createCommand(RequestType.Copy, args); } +/** + * @internal + */ +export function createMove( + key: string, + dbIndex: number, +): command_request.Command { + return createCommand(RequestType.Move, [key, dbIndex.toString()]); +} + /** * Optional arguments to LPOS command. * diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index daedf2935d..cda9ec6cfa 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -38,6 +38,7 @@ import { createInfo, createLastSave, createLolwut, + createMove, createPing, createPublish, createRandomKey, @@ -426,6 +427,26 @@ export class GlideClient extends BaseClient { ); } + /** + * Move `key` from the currently selected database to the database specified by `dbIndex`. + * + * See https://valkey.io/commands/move/ for more details. + * + * @param key - The key to move. + * @param dbIndex - The index of the database to move `key` to. + * @returns `true` if `key` was moved, or `false` if the `key` already exists in the destination + * database or does not exist in the source database. + * + * @example + * ```typescript + * const result = await client.move("key", 1); + * console.log(result); // Output: true + * ``` + */ + public async move(key: string, dbIndex: number): Promise { + return this.createWritePromise(createMove(key, dbIndex)); + } + /** * Displays a piece of generative computer art and the server version. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0004c600a8..4b74520910 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -141,6 +141,7 @@ import { createMGet, createMSet, createMSetNX, + createMove, createObjectEncoding, createObjectFreq, createObjectIdletime, @@ -3418,6 +3419,21 @@ export class Transaction extends BaseTransaction { return this.addAndReturn(createCopy(source, destination, options)); } + /** + * Move `key` from the currently selected database to the database specified by `dbIndex`. + * + * See https://valkey.io/commands/move/ for more details. + * + * @param key - The key to move. + * @param dbIndex - The index of the database to move `key` to. + * + * Command Response - `true` if `key` was moved, or `false` if the `key` already exists in the destination + * database or does not exist in the source database. + */ + public move(key: string, dbIndex: number): Transaction { + return this.addAndReturn(createMove(key, dbIndex)); + } + /** Publish a message on pubsub channel. * * See https://valkey.io/commands/publish for more details. diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index d74776ced0..77125333ae 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -504,6 +504,46 @@ describe("GlideClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "move test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const value = uuidv4(); + + expect(await client.select(0)).toEqual("OK"); + expect(await client.move(key1, 1)).toEqual(false); + + expect(await client.set(key1, value)).toEqual("OK"); + expect(await client.get(key1)).toEqual(value); + expect(await client.move(key1, 1)).toEqual(true); + expect(await client.get(key1)).toEqual(null); + expect(await client.select(1)).toEqual("OK"); + expect(await client.get(key1)).toEqual(value); + + await expect(client.move(key1, -1)).rejects.toThrow(RequestError); + + //transaction tests + const transaction = new Transaction(); + transaction.select(1); + transaction.move(key2, 0); + transaction.set(key2, value); + transaction.move(key2, 0); + transaction.select(0); + transaction.get(key2); + const results = await client.exec(transaction); + + expect(results).toEqual(["OK", false, "OK", true, "OK", value]); + + client.close(); + }, + TIMEOUT, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "function load function list function stats test_%p", async (protocol) => { From ce69945ded80b57d1680c345c50d743661d42620 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 9 Aug 2024 11:41:40 -0700 Subject: [PATCH 162/236] Node: Add `XPENDING` command. (#2085) * Add `XPENDING` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 +- node/src/BaseClient.ts | 73 +++++++++++++++++++ node/src/Commands.ts | 138 +++++++++++++++++++++++++----------- node/src/Transaction.ts | 38 ++++++++++ node/tests/SharedTests.ts | 100 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 14 ++++ 7 files changed, 327 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8b35ef31..4531e039ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XPENDING commands ([#2085](https://github.com/valkey-io/valkey-glide/pull/2085)) * Node: Added XINFO CONSUMERS command ([#2093](https://github.com/valkey-io/valkey-glide/pull/2093)) * Node: Added HRANDFIELD command ([#2096](https://github.com/valkey-io/valkey-glide/pull/2096)) * Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index a78c0bdd0c..a4686e774b 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -140,6 +140,7 @@ function initialize() { StreamAddOptions, StreamReadOptions, StreamClaimOptions, + StreamPendingOptions, ScriptOptions, ClosingError, ConfigurationError, @@ -232,8 +233,9 @@ function initialize() { StreamGroupOptions, StreamTrimOptions, StreamAddOptions, - StreamReadOptions, StreamClaimOptions, + StreamReadOptions, + StreamPendingOptions, ScriptOptions, ClosingError, ConfigurationError, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index f2bbe54e4a..549f81ea37 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -48,6 +48,7 @@ import { StreamAddOptions, StreamClaimOptions, StreamGroupOptions, + StreamPendingOptions, StreamReadOptions, StreamTrimOptions, ZAddOptions, @@ -170,6 +171,7 @@ import { createXGroupCreateConsumer, createXGroupDelConsumer, createXLen, + createXPending, createXRead, createXTrim, createZAdd, @@ -3925,6 +3927,77 @@ export class BaseClient { return this.createWritePromise(createXLen(key)); } + /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * See https://valkey.io/commands/xpending/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @returns An `array` that includes the summary of the pending messages. See example for more details. + * @example + * ```typescript + * console.log(await client.xpending("my_stream", "my_group")); // Output: + * // [ + * // 42, // The total number of pending messages + * // "1722643465939-0", // The smallest ID among the pending messages + * // "1722643484626-0", // The greatest ID among the pending messages + * // [ // A 2D-`array` of every consumer in the group + * // [ "consumer1", "10" ], // with at least one pending message, and the + * // [ "consumer2", "32" ], // number of pending messages it has + * // ] + * // ] + * ``` + */ + public async xpending( + key: string, + group: string, + ): Promise<[number, string, string, [string, number][]]> { + return this.createWritePromise(createXPending(key, group)); + } + + /** + * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * + * See https://valkey.io/commands/xpending/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param options - Additional options to filter entries, see {@link StreamPendingOptions}. + * @returns A 2D-`array` of 4-tuples containing extended message information. See example for more details. + * + * @example + * ```typescript + * console.log(await client.xpending("my_stream", "my_group"), { + * start: { value: "0-1", isInclusive: true }, + * end: InfScoreBoundary.PositiveInfinity, + * count: 2, + * consumer: "consumer1" + * }); // Output: + * // [ + * // [ + * // "1722643465939-0", // The ID of the message + * // "consumer1", // The name of the consumer that fetched the message and has still to acknowledge it + * // 174431, // The number of milliseconds that elapsed since the last time this message was delivered to this consumer + * // 1 // The number of times this message was delivered + * // ], + * // [ + * // "1722643484626-0", + * // "consumer1", + * // 202231, + * // 1 + * // ] + * // ] + * ``` + */ + public async xpendingWithOptions( + key: string, + group: string, + options: StreamPendingOptions, + ): Promise<[string, string, number, number][]> { + return this.createWritePromise(createXPending(key, group, options)); + } + /** * Returns the list of all consumers and their attributes for the given consumer group of the * stream stored at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 489eb198a1..ca00417ac9 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1628,37 +1628,52 @@ type SortedSetRange = { export type RangeByScore = SortedSetRange & { type: "byScore" }; export type RangeByLex = SortedSetRange & { type: "byLex" }; -/** - * Returns a string representation of a score boundary in Redis protocol format. - * @param score - The score boundary object containing value and inclusivity - * information. - * @param isLex - Indicates whether to return lexical representation for - * positive/negative infinity. - * @returns A string representation of the score boundary in Redis protocol - * format. - */ +/** Returns a string representation of a score boundary as a command argument. */ function getScoreBoundaryArg( score: ScoreBoundary | ScoreBoundary, - isLex: boolean = false, ): string { - if (score == InfScoreBoundary.PositiveInfinity) { - return ( - InfScoreBoundary.PositiveInfinity.toString() + (isLex ? "" : "inf") - ); + if (typeof score === "string") { + // InfScoreBoundary + return score + "inf"; } - if (score == InfScoreBoundary.NegativeInfinity) { - return ( - InfScoreBoundary.NegativeInfinity.toString() + (isLex ? "" : "inf") - ); + if (score.isInclusive == false) { + return "(" + score.value.toString(); + } + + return score.value.toString(); +} + +/** Returns a string representation of a lex boundary as a command argument. */ +function getLexBoundaryArg( + score: ScoreBoundary | ScoreBoundary, +): string { + if (typeof score === "string") { + // InfScoreBoundary + return score; + } + + if (score.isInclusive == false) { + return "(" + score.value.toString(); + } + + return "[" + score.value.toString(); +} + +/** Returns a string representation of a stream boundary as a command argument. */ +function getStreamBoundaryArg( + score: ScoreBoundary | ScoreBoundary, +): string { + if (typeof score === "string") { + // InfScoreBoundary + return score; } if (score.isInclusive == false) { return "(" + score.value.toString(); } - const value = isLex ? "[" + score.value.toString() : score.value.toString(); - return value; + return score.value.toString(); } function createZRangeArgs( @@ -1671,10 +1686,20 @@ function createZRangeArgs( if (typeof rangeQuery.start != "number") { rangeQuery = rangeQuery as RangeByScore | RangeByLex; - const isLex = rangeQuery.type == "byLex"; - args.push(getScoreBoundaryArg(rangeQuery.start, isLex)); - args.push(getScoreBoundaryArg(rangeQuery.stop, isLex)); - args.push(isLex == true ? "BYLEX" : "BYSCORE"); + + if (rangeQuery.type == "byLex") { + args.push( + getLexBoundaryArg(rangeQuery.start), + getLexBoundaryArg(rangeQuery.stop), + "BYLEX", + ); + } else { + args.push( + getScoreBoundaryArg(rangeQuery.start), + getScoreBoundaryArg(rangeQuery.stop), + "BYSCORE", + ); + } } else { args.push(rangeQuery.start.toString()); args.push(rangeQuery.stop.toString()); @@ -1707,9 +1732,11 @@ export function createZCount( minScore: ScoreBoundary, maxScore: ScoreBoundary, ): command_request.Command { - const args = [key]; - args.push(getScoreBoundaryArg(minScore)); - args.push(getScoreBoundaryArg(maxScore)); + const args = [ + key, + getScoreBoundaryArg(minScore), + getScoreBoundaryArg(maxScore), + ]; return createCommand(RequestType.ZCount, args); } @@ -1862,11 +1889,7 @@ export function createZRemRangeByLex( minLex: ScoreBoundary, maxLex: ScoreBoundary, ): command_request.Command { - const args = [ - key, - getScoreBoundaryArg(minLex, true), - getScoreBoundaryArg(maxLex, true), - ]; + const args = [key, getLexBoundaryArg(minLex), getLexBoundaryArg(maxLex)]; return createCommand(RequestType.ZRemRangeByLex, args); } @@ -1878,12 +1901,15 @@ export function createZRemRangeByScore( minScore: ScoreBoundary, maxScore: ScoreBoundary, ): command_request.Command { - const args = [key]; - args.push(getScoreBoundaryArg(minScore)); - args.push(getScoreBoundaryArg(maxScore)); + const args = [ + key, + getScoreBoundaryArg(minScore), + getScoreBoundaryArg(maxScore), + ]; return createCommand(RequestType.ZRemRangeByScore, args); } +/** @internal */ export function createPersist(key: string): command_request.Command { return createCommand(RequestType.Persist, [key]); } @@ -1896,11 +1922,7 @@ export function createZLexCount( minLex: ScoreBoundary, maxLex: ScoreBoundary, ): command_request.Command { - const args = [ - key, - getScoreBoundaryArg(minLex, true), - getScoreBoundaryArg(maxLex, true), - ]; + const args = [key, getLexBoundaryArg(minLex), getLexBoundaryArg(maxLex)]; return createCommand(RequestType.ZLexCount, args); } @@ -2417,6 +2439,42 @@ export function createXLen(key: string): command_request.Command { return createCommand(RequestType.XLen, [key]); } +/** Optional arguments for {@link BaseClient.xpendingWithOptions|xpending}. */ +export type StreamPendingOptions = { + /** Filter pending entries by their idle time - in milliseconds */ + minIdleTime?: number; + /** Starting stream ID bound for range. */ + start: ScoreBoundary; + /** Ending stream ID bound for range. */ + end: ScoreBoundary; + /** Limit the number of messages returned. */ + count: number; + /** Filter pending entries by consumer. */ + consumer?: string; +}; + +/** @internal */ +export function createXPending( + key: string, + group: string, + options?: StreamPendingOptions, +): command_request.Command { + const args = [key, group]; + + if (options) { + if (options.minIdleTime !== undefined) + args.push("IDLE", options.minIdleTime.toString()); + args.push( + getStreamBoundaryArg(options.start), + getStreamBoundaryArg(options.end), + options.count.toString(), + ); + if (options.consumer) args.push(options.consumer); + } + + return createCommand(RequestType.XPending, args); +} + /** @internal */ export function createXInfoConsumers( key: string, diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 4b74520910..e4fe1f5674 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -54,6 +54,7 @@ import { StreamAddOptions, StreamClaimOptions, StreamGroupOptions, + StreamPendingOptions, StreamReadOptions, StreamTrimOptions, ZAddOptions, @@ -205,6 +206,7 @@ import { createXInfoConsumers, createXInfoStream, createXLen, + createXPending, createXRead, createXTrim, createZAdd, @@ -2312,6 +2314,9 @@ export class BaseTransaction> { } /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * See https://valkey.io/commands/xpending/ for more details. * Returns the list of all consumers and their attributes for the given consumer group of the * stream stored at `key`. * @@ -2320,6 +2325,39 @@ export class BaseTransaction> { * @param key - The key of the stream. * @param group - The consumer group name. * + * Command Response - An `array` that includes the summary of the pending messages. + * See example of {@link BaseClient.xpending|xpending} for more details. + */ + public xpending(key: string, group: string): T { + return this.addAndReturn(createXPending(key, group)); + } + + /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * See https://valkey.io/commands/xpending/ for more details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param options - Additional options to filter entries, see {@link StreamPendingOptions}. + * + * Command Response - A 2D-`array` of 4-tuples containing extended message information. + * See example of {@link BaseClient.xpendingWithOptions|xpendingWithOptions} for more details. + */ + public xpendingWithOptions( + key: string, + group: string, + options: StreamPendingOptions, + ): T { + return this.addAndReturn(createXPending(key, group, options)); + } + + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at `key`. + * + * See https://valkey.io/commands/xinfo-consumers/ for more details. + * * Command Response - An `Array` of `Records`, where each mapping contains the attributes * of a consumer for the given consumer group of the stream at `key`. */ diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1bfdbada9d..47c6e1cbb6 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -7678,6 +7678,106 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xpending test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const group = uuidv4(); + + expect( + await client.xgroupCreate(key, group, "0", { + mkStream: true, + }), + ).toEqual("OK"); + expect( + await client.customCommand([ + "xgroup", + "createconsumer", + key, + group, + "consumer", + ]), + ).toEqual(true); + + expect( + await client.xadd( + key, + [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + { id: "0-1" }, + ), + ).toEqual("0-1"); + expect( + await client.xadd( + key, + [["entry2_field1", "entry2_value1"]], + { id: "0-2" }, + ), + ).toEqual("0-2"); + + expect( + await client.customCommand([ + "xreadgroup", + "group", + group, + "consumer", + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + "0-1": [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + "0-2": [["entry2_field1", "entry2_value1"]], + }, + }); + + // wait to get some minIdleTime + await new Promise((resolve) => setTimeout(resolve, 500)); + + expect(await client.xpending(key, group)).toEqual([ + 2, + "0-1", + "0-2", + [["consumer", "2"]], + ]); + + const result = await client.xpendingWithOptions(key, group, { + start: InfScoreBoundary.NegativeInfinity, + end: InfScoreBoundary.PositiveInfinity, + count: 1, + minIdleTime: 42, + }); + result[0][2] = 0; // overwrite msec counter to avoid test flakyness + expect(result).toEqual([["0-1", "consumer", 0, 1]]); + + // not existing consumer + expect( + await client.xpendingWithOptions(key, group, { + start: { value: "0-1", isInclusive: true }, + end: { value: "0-2", isInclusive: false }, + count: 12, + consumer: "_", + }), + ).toEqual([]); + + // key exists, but it is not a stream + const stringKey = uuidv4(); + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect(client.xpending(stringKey, "_")).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `xclaim test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ec2090f520..9660d06809 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -983,6 +983,20 @@ export async function transactionTest( 'xreadgroup(groupName1, "consumer1", key9, >)', { [key9]: { "0-2": [["field", "value2"]] } }, ]); + baseTransaction.xpending(key9, groupName1); + responseData.push([ + "xpending(key9, groupName1)", + [1, "0-2", "0-2", [["consumer1", "1"]]], + ]); + baseTransaction.xpendingWithOptions(key9, groupName1, { + start: InfScoreBoundary.NegativeInfinity, + end: InfScoreBoundary.PositiveInfinity, + count: 10, + }); + responseData.push([ + "xpending(key9, groupName1, -, +, 10)", + [["0-2", "consumer1", 0, 1]], + ]); baseTransaction.xclaim(key9, groupName1, "consumer1", 0, ["0-2"]); responseData.push([ 'xclaim(key9, groupName1, "consumer1", 0, ["0-2"])', From e4e39ddb33667ca17bbc9dc854a7482204bb1516 Mon Sep 17 00:00:00 2001 From: adarovadya Date: Mon, 12 Aug 2024 16:14:20 +0300 Subject: [PATCH 163/236] Node: add Decoder enum (#2052) * Node: add decoder enum per command and optional client config. add transection decoder --------- Signed-off-by: Adar Ovadia Co-authored-by: Adar Ovadia --- node/src/BaseClient.ts | 124 +++++++++++++++--- node/src/Commands.ts | 23 ++-- node/src/GlideClient.ts | 20 ++- node/src/GlideClusterClient.ts | 165 ++++++++++++------------ node/src/Transaction.ts | 3 +- node/tests/GlideClient.test.ts | 145 +++++++++++++++++---- node/tests/GlideClientInternals.test.ts | 29 +++-- node/tests/GlideClusterClient.test.ts | 72 ++++++++--- node/tests/SharedTests.ts | 28 ++++ node/tests/TestUtilities.ts | 70 ++++++++++ 10 files changed, 513 insertions(+), 166 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 549f81ea37..4ff9b6f9b9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -8,7 +8,7 @@ import { valueFromSplitPointer, } from "glide-rs"; import * as net from "net"; -import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; +import { Buffer, BufferWriter, Long, Reader, Writer } from "protobufjs"; import { AggregationType, BaseScanOptions, @@ -165,11 +165,11 @@ import { createXClaim, createXDel, createXGroupCreate, + createXGroupCreateConsumer, + createXGroupDelConsumer, createXGroupDestroy, createXInfoConsumers, createXInfoStream, - createXGroupCreateConsumer, - createXGroupDelConsumer, createXLen, createXPending, createXRead, @@ -249,6 +249,44 @@ export type ReturnType = | ReturnTypeAttribute | ReturnType[]; +export type GlideString = string | Uint8Array; + +/** + * Enum representing the different types of decoders. + */ +export const enum Decoder { + /** + * Decodes the response into a buffer array. + */ + Bytes, + /** + * Decodes the response into a string. + */ + String, +} + +/** + * Our purpose in creating PointerResponse type is to mark when response is of number/long pointer response type. + * Consequently, when the response is returned, we can check whether it is instanceof the PointerResponse type and pass it to the Rust core function with the proper parameters. + */ +class PointerResponse { + pointer: number | Long | null; + // As Javascript does not support 64-bit integers, + // we split the Rust u64 pointer into two u32 integers (high and low) and build it again when we call value_from_split_pointer, the Rust function. + high: number | undefined; + low: number | undefined; + + constructor( + pointer: number | Long | null, + high?: number | undefined, + low?: number | undefined, + ) { + this.pointer = pointer; + this.high = high; + this.low = low; + } +} + /** Represents the credentials for connecting to a server. */ export type RedisCredentials = { /** @@ -329,6 +367,11 @@ export type BaseClientConfiguration = { * Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. */ clientName?: string; + /** + * Default decoder when decoder is not set per command. + * If not set, 'Decoder.String' will be used. + */ + defaultDecoder?: Decoder; }; export type ScriptOptions = { @@ -369,6 +412,11 @@ export type PubSubMsg = { channel: string; pattern?: string | null; }; + +export type WritePromiseOptions = { + decoder?: Decoder; + route?: command_request.Routes; +}; export class BaseClient { private socket: net.Socket; private readonly promiseCallbackFunctions: [ @@ -381,6 +429,7 @@ export class BaseClient { private remainingReadData: Uint8Array | undefined; private readonly requestTimeout: number; // Timeout in milliseconds private isClosed = false; + protected defaultDecoder = Decoder.String; private readonly pubsubFutures: [PromiseFunction, ErrorFunction][] = []; private pendingPushNotification: response.Response[] = []; private config: BaseClientConfiguration | undefined; @@ -480,15 +529,21 @@ export class BaseClient { const errorType = getRequestErrorClass(message.requestError.type); reject(new errorType(message.requestError.message ?? undefined)); } else if (message.respPointer != null) { - const pointer = message.respPointer; + let pointer; - if (typeof pointer === "number") { - // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 - resolve(valueFromSplitPointer(0, pointer, true)); + if (typeof message.respPointer === "number") { + // Response from type number + pointer = new PointerResponse(message.respPointer); } else { - // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 - resolve(valueFromSplitPointer(pointer.high, pointer.low, true)); + // Response from type long + pointer = new PointerResponse( + message.respPointer, + message.respPointer.high, + message.respPointer.low, + ); } + + resolve(pointer); } else if (message.constantResponse === response.ConstantResponse.OK) { resolve("OK"); } else { @@ -542,6 +597,7 @@ export class BaseClient { console.error(`Server closed: ${err}`); this.close(); }); + this.defaultDecoder = options?.defaultDecoder ?? Decoder.String; } private getCallbackIndex(): number { @@ -573,8 +629,11 @@ export class BaseClient { | command_request.Command | command_request.Command[] | command_request.ScriptInvocation, - route?: command_request.Routes, + options: WritePromiseOptions = {}, ): Promise { + const { decoder = this.defaultDecoder, route } = options; + const stringDecoder = decoder === Decoder.String ? true : false; + if (this.isClosed) { throw new ClosingError( "Unable to execute requests; the client is closed. Please create a new client.", @@ -583,7 +642,28 @@ export class BaseClient { return new Promise((resolve, reject) => { const callbackIndex = this.getCallbackIndex(); - this.promiseCallbackFunctions[callbackIndex] = [resolve, reject]; + this.promiseCallbackFunctions[callbackIndex] = [ + (resolveAns: T) => { + if (resolveAns instanceof PointerResponse) { + if (typeof resolveAns === "number") { + resolveAns = valueFromSplitPointer( + 0, + resolveAns, + stringDecoder, + ) as T; + } else { + resolveAns = valueFromSplitPointer( + resolveAns.high!, + resolveAns.low!, + stringDecoder, + ) as T; + } + } + + resolve(resolveAns); + }, + reject, + ]; this.writeOrBufferCommandRequest(callbackIndex, command, route); }); } @@ -710,7 +790,7 @@ export class BaseClient { }); } - public tryGetPubSubMessage(): PubSubMsg | null { + public tryGetPubSubMessage(decoder?: Decoder): PubSubMsg | null { if (this.isClosed) { throw new ClosingError( "Unable to execute requests; the client is closed. Please create a new client.", @@ -734,13 +814,17 @@ export class BaseClient { while (this.pendingPushNotification.length > 0 && !msg) { const pushNotification = this.pendingPushNotification.shift()!; - msg = this.notificationToPubSubMessageSafe(pushNotification); + msg = this.notificationToPubSubMessageSafe( + pushNotification, + decoder, + ); } return msg; } notificationToPubSubMessageSafe( pushNotification: response.Response, + decoder?: Decoder, ): PubSubMsg | null { let msg: PubSubMsg | null = null; const responsePointer = pushNotification.respPointer; @@ -751,15 +835,13 @@ export class BaseClient { nextPushNotificationValue = valueFromSplitPointer( responsePointer.high, responsePointer.low, - // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 - true, + decoder === Decoder.String, ) as Record; } else { nextPushNotificationValue = valueFromSplitPointer( 0, responsePointer, - // TODO: change according to https://github.com/valkey-io/valkey-glide/pull/2052 - true, + decoder === Decoder.String, ) as Record; } @@ -831,6 +913,7 @@ export class BaseClient { * See https://valkey.io/commands/get/ for details. * * @param key - The key to retrieve from the database. + * @param decoder - Optional enum parameter for decoding the response. * @returns If `key` exists, returns the value of `key` as a string. Otherwise, return null. * * @example @@ -838,10 +921,13 @@ export class BaseClient { * // Example usage of get method to retrieve the value of a key * const result = await client.get("key"); * console.log(result); // Output: 'value' + * // Example usage of get method to retrieve the value of a key with Bytes decoder + * const result = await client.get("key", Decoder.Bytes); + * console.log(result); // Output: {"data": [118, 97, 108, 117, 101], "type": "Buffer"} * ``` */ - public get(key: string): Promise { - return this.createWritePromise(createGet(key)); + public get(key: string, decoder?: Decoder): Promise { + return this.createWritePromise(createGet(key), { decoder: decoder }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ca00417ac9..584ffb0a60 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -6,16 +6,17 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { BaseClient } from "src/BaseClient"; +import { BaseClient, Decoder } from "src/BaseClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { GlideClient } from "src/GlideClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { GlideClusterClient } from "src/GlideClusterClient"; +import { GlideString } from "./BaseClient"; import { command_request } from "./ProtobufMessage"; import RequestType = command_request.RequestType; -function isLargeCommand(args: BulkString[]) { +function isLargeCommand(args: GlideString[]) { let lenSum = 0; for (const arg of args) { @@ -29,12 +30,10 @@ function isLargeCommand(args: BulkString[]) { return false; } -type BulkString = string | Uint8Array; - /** * Convert a string array into Uint8Array[] */ -function toBuffersArray(args: BulkString[]) { +function toBuffersArray(args: GlideString[]) { const argsBytes: Uint8Array[] = []; for (const arg of args) { @@ -68,7 +67,7 @@ export function parseInfoResponse(response: string): Record { function createCommand( requestType: command_request.RequestType, - args: BulkString[], + args: GlideString[], ): command_request.Command { const singleCommand = command_request.Command.create({ requestType, @@ -171,8 +170,8 @@ export type SetOptions = { * @internal */ export function createSet( - key: BulkString, - value: BulkString, + key: GlideString, + value: GlideString, options?: SetOptions, ): command_request.Command { const args = [key, value]; @@ -1182,7 +1181,7 @@ export function createSRandMember( /** * @internal */ -export function createCustomCommand(args: string[]) { +export function createCustomCommand(args: GlideString[]) { return createCommand(RequestType.CustomCommand, args); } @@ -2688,6 +2687,12 @@ export type LolwutOptions = { * For version `6`, those are number of columns and number of lines. */ parameters?: number[]; + /** + * An optional argument specifies the type of decoding. + * Use Decoder.String to get the response as a String. + * Use Decoder.Bytes to get the response in a buffer. + */ + decoder?: Decoder; }; /** diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index cda9ec6cfa..bf631b898d 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -6,6 +6,8 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, + Decoder, + GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, @@ -170,14 +172,19 @@ export class GlideClient extends BaseClient { * See https://redis.io/topics/Transactions/ for details on Redis Transactions. * * @param transaction - A Transaction object containing a list of commands to be executed. + * @param decoder - An optional parameter to decode all commands in the transaction. If not set, 'Decoder.String' will be used. * @returns A list of results corresponding to the execution of each command in the transaction. * If a command returns a value, it will be included in the list. If a command doesn't return a value, * the list entry will be null. * If the transaction failed due to a WATCH command, `exec` will return `null`. */ - public exec(transaction: Transaction): Promise { + public exec( + transaction: Transaction, + decoder: Decoder = this.defaultDecoder, + ): Promise { return this.createWritePromise( transaction.commands, + { decoder: decoder }, ).then((result: ReturnType[] | null) => { return this.processResultWithSetCommands( result, @@ -189,6 +196,8 @@ export class GlideClient extends BaseClient { /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * + * Note: An error will occur if the string decoder is used with commands that return only bytes as a response. + * * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) * for details on the restrictions and limitations of the custom command API. * @@ -199,8 +208,13 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: Returns a list of all pub/sub clients * ``` */ - public customCommand(args: string[]): Promise { - return this.createWritePromise(createCustomCommand(args)); + public customCommand( + args: GlideString[], + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createCustomCommand(args), { + decoder: decoder, + }); } /** Ping the Redis server. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index bfd00dbc5f..4cf01a7073 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -6,6 +6,8 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, + Decoder, + GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, @@ -341,19 +343,27 @@ export class GlideClusterClient extends BaseClient { * The command will be routed automatically based on the passed command's default request policy, unless `route` is provided, * in which case the client will route the command to the nodes defined by `route`. * - * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + * Note: An error will occur if the string decoder is used with commands that return only bytes as a response. + * + * See the [Glide for Valkey Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) * for details on the restrictions and limitations of the custom command API. * * @example * ```typescript * // Example usage of customCommand method to retrieve pub/sub clients with routing to all primary nodes - * const result = await client.customCommand(["CLIENT", "LIST", "TYPE", "PUBSUB"], "allPrimaries"); + * const result = await client.customCommand(["CLIENT", "LIST", "TYPE", "PUBSUB"], {route: "allPrimaries", decoder: Decoder.String}); * console.log(result); // Output: Returns a list of all pub/sub clients * ``` */ - public customCommand(args: string[], route?: Routes): Promise { + public customCommand( + args: GlideString[], + options?: { route?: Routes; decoder?: Decoder }, + ): Promise { const command = createCustomCommand(args); - return super.createWritePromise(command, toProtobufRoute(route)); + return super.createWritePromise(command, { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, + }); } /** Execute a transaction by processing the queued commands. @@ -370,11 +380,17 @@ export class GlideClusterClient extends BaseClient { */ public exec( transaction: ClusterTransaction, - route?: SingleNodeRoute, + options?: { + route?: SingleNodeRoute; + decoder?: Decoder; + }, ): Promise { return this.createWritePromise( transaction.commands, - toProtobufRoute(route), + { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, + }, ).then((result: ReturnType[] | null) => { return this.processResultWithSetCommands( result, @@ -408,10 +424,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public ping(message?: string, route?: Routes): Promise { - return this.createWritePromise( - createPing(message), - toProtobufRoute(route), - ); + return this.createWritePromise(createPing(message), { + route: toProtobufRoute(route), + }); } /** Get information and statistics about the Redis server. @@ -430,7 +445,7 @@ export class GlideClusterClient extends BaseClient { ): Promise> { return this.createWritePromise>( createInfo(options), - toProtobufRoute(route), + { route: toProtobufRoute(route) }, ); } @@ -463,7 +478,7 @@ export class GlideClusterClient extends BaseClient { ): Promise> { return this.createWritePromise>( createClientGetName(), - toProtobufRoute(route), + { route: toProtobufRoute(route) }, ); } @@ -483,10 +498,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public configRewrite(route?: Routes): Promise<"OK"> { - return this.createWritePromise( - createConfigRewrite(), - toProtobufRoute(route), - ); + return this.createWritePromise(createConfigRewrite(), { + route: toProtobufRoute(route), + }); } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. @@ -505,10 +519,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public configResetStat(route?: Routes): Promise<"OK"> { - return this.createWritePromise( - createConfigResetStat(), - toProtobufRoute(route), - ); + return this.createWritePromise(createConfigResetStat(), { + route: toProtobufRoute(route), + }); } /** Returns the current connection id. @@ -522,7 +535,7 @@ export class GlideClusterClient extends BaseClient { public clientId(route?: Routes): Promise> { return this.createWritePromise>( createClientId(), - toProtobufRoute(route), + { route: toProtobufRoute(route) }, ); } @@ -557,7 +570,7 @@ export class GlideClusterClient extends BaseClient { ): Promise>> { return this.createWritePromise>>( createConfigGet(parameters), - toProtobufRoute(route), + { route: toProtobufRoute(route) }, ); } @@ -582,10 +595,9 @@ export class GlideClusterClient extends BaseClient { parameters: Record, route?: Routes, ): Promise<"OK"> { - return this.createWritePromise( - createConfigSet(parameters), - toProtobufRoute(route), - ); + return this.createWritePromise(createConfigSet(parameters), { + route: toProtobufRoute(route), + }); } /** Echoes the provided `message` back. @@ -614,10 +626,9 @@ export class GlideClusterClient extends BaseClient { message: string, route?: Routes, ): Promise> { - return this.createWritePromise( - createEcho(message), - toProtobufRoute(route), - ); + return this.createWritePromise(createEcho(message), { + route: toProtobufRoute(route), + }); } /** Returns the server time. @@ -647,7 +658,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public time(route?: Routes): Promise> { - return this.createWritePromise(createTime(), toProtobufRoute(route)); + return this.createWritePromise(createTime(), { + route: toProtobufRoute(route), + }); } /** @@ -701,10 +714,10 @@ export class GlideClusterClient extends BaseClient { options?: LolwutOptions, route?: Routes, ): Promise> { - return this.createWritePromise( - createLolwut(options), - toProtobufRoute(route), - ); + return this.createWritePromise(createLolwut(options), { + decoder: options?.decoder, + route: toProtobufRoute(route), + }); } /** @@ -731,10 +744,9 @@ export class GlideClusterClient extends BaseClient { args: string[], route?: Routes, ): Promise { - return this.createWritePromise( - createFCall(func, [], args), - toProtobufRoute(route), - ); + return this.createWritePromise(createFCall(func, [], args), { + route: toProtobufRoute(route), + }); } /** @@ -762,10 +774,9 @@ export class GlideClusterClient extends BaseClient { args: string[], route?: Routes, ): Promise { - return this.createWritePromise( - createFCallReadOnly(func, [], args), - toProtobufRoute(route), - ); + return this.createWritePromise(createFCallReadOnly(func, [], args), { + route: toProtobufRoute(route), + }); } /** @@ -790,10 +801,9 @@ export class GlideClusterClient extends BaseClient { libraryCode: string, route?: Routes, ): Promise { - return this.createWritePromise( - createFunctionDelete(libraryCode), - toProtobufRoute(route), - ); + return this.createWritePromise(createFunctionDelete(libraryCode), { + route: toProtobufRoute(route), + }); } /** @@ -824,7 +834,7 @@ export class GlideClusterClient extends BaseClient { ): Promise { return this.createWritePromise( createFunctionLoad(libraryCode, replace), - toProtobufRoute(route), + { route: toProtobufRoute(route) }, ); } @@ -847,10 +857,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public functionFlush(mode?: FlushMode, route?: Routes): Promise { - return this.createWritePromise( - createFunctionFlush(mode), - toProtobufRoute(route), - ); + return this.createWritePromise(createFunctionFlush(mode), { + route: toProtobufRoute(route), + }); } /** @@ -888,10 +897,9 @@ export class GlideClusterClient extends BaseClient { options?: FunctionListOptions, route?: Routes, ): Promise> { - return this.createWritePromise( - createFunctionList(options), - toProtobufRoute(route), - ); + return this.createWritePromise(createFunctionList(options), { + route: toProtobufRoute(route), + }); } /** @@ -947,10 +955,9 @@ export class GlideClusterClient extends BaseClient { public async functionStats( route?: Routes, ): Promise> { - return this.createWritePromise( - createFunctionStats(), - toProtobufRoute(route), - ); + return this.createWritePromise(createFunctionStats(), { + route: toProtobufRoute(route), + }); } /** @@ -970,10 +977,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public flushall(mode?: FlushMode, route?: Routes): Promise { - return this.createWritePromise( - createFlushAll(mode), - toProtobufRoute(route), - ); + return this.createWritePromise(createFlushAll(mode), { + route: toProtobufRoute(route), + }); } /** @@ -993,10 +999,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public flushdb(mode?: FlushMode, route?: Routes): Promise { - return this.createWritePromise( - createFlushDB(mode), - toProtobufRoute(route), - ); + return this.createWritePromise(createFlushDB(mode), { + route: toProtobufRoute(route), + }); } /** @@ -1015,8 +1020,10 @@ export class GlideClusterClient extends BaseClient { * console.log("Number of keys across all primary nodes: ", numKeys); * ``` */ - public dbsize(route?: Routes): Promise { - return this.createWritePromise(createDBSize(), toProtobufRoute(route)); + public dbsize(route?: Routes): Promise> { + return this.createWritePromise(createDBSize(), { + route: toProtobufRoute(route), + }); } /** Publish a message on pubsub channel. @@ -1211,10 +1218,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async lastsave(route?: Routes): Promise> { - return this.createWritePromise( - createLastSave(), - toProtobufRoute(route), - ); + return this.createWritePromise(createLastSave(), { + route: toProtobufRoute(route), + }); } /** @@ -1233,10 +1239,9 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async randomKey(route?: Routes): Promise { - return this.createWritePromise( - createRandomKey(), - toProtobufRoute(route), - ); + return this.createWritePromise(createRandomKey(), { + route: toProtobufRoute(route), + }); } /** @@ -1258,6 +1263,8 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async unwatch(route?: Routes): Promise<"OK"> { - return this.createWritePromise(createUnWatch(), toProtobufRoute(route)); + return this.createWritePromise(createUnWatch(), { + route: toProtobufRoute(route), + }); } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e4fe1f5674..65266f1109 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars + GlideString, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; @@ -2169,7 +2170,7 @@ export class BaseTransaction> { * * Command Response - A response from Redis with an `Object`. */ - public customCommand(args: string[]): T { + public customCommand(args: GlideString[]): T { return this.addAndReturn(createCustomCommand(args)); } diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 77125333ae..656481bd53 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -13,6 +13,7 @@ import { import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { + Decoder, GlideClient, ListDirection, ProtocolVersion, @@ -27,10 +28,11 @@ import { checkFunctionListResponse, checkFunctionStatsResponse, convertStringArrayToBuffer, + encodableTransactionTest, + encodedTransactionTest, flushAndCloseClient, generateLuaLibCode, getClientConfigurationOption, - intoString, parseCommandLineArgs, parseEndpoints, transactionTest, @@ -120,13 +122,9 @@ describe("GlideClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const result = await client.info(); - expect(intoString(result)).toEqual( - expect.stringContaining("# Server"), - ); - expect(intoString(result)).toEqual( - expect.stringContaining("# Replication"), - ); - expect(intoString(result)).toEqual( + expect(result).toEqual(expect.stringContaining("# Server")); + expect(result).toEqual(expect.stringContaining("# Replication")); + expect(result).toEqual( expect.not.stringContaining("# Latencystats"), ); }, @@ -201,6 +199,52 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "bytes decoder client test %p", + async (protocol) => { + const clientConfig = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + clientConfig.defaultDecoder = Decoder.Bytes; + client = await GlideClient.createClient(clientConfig); + expect(await client.select(0)).toEqual("OK"); + + const key = uuidv4(); + const value = uuidv4(); + const valueEncoded = Buffer.from(value); + const result = await client.set(key, value); + expect(result).toEqual("OK"); + + expect(await client.get(key)).toEqual(valueEncoded); + expect(await client.get(key, Decoder.String)).toEqual(value); + expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "string decoder client test %p", + async (protocol) => { + const clientConfig = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + clientConfig.defaultDecoder = Decoder.String; + client = await GlideClient.createClient(clientConfig); + expect(await client.select(0)).toEqual("OK"); + + const key = uuidv4(); + const value = uuidv4(); + const valueEncoded = Buffer.from(value); + const result = await client.set(key, value); + expect(result).toEqual("OK"); + + expect(await client.get(key)).toEqual(value); + expect(await client.get(key, Decoder.String)).toEqual(value); + expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `can send transactions_%p`, async (protocol) => { @@ -220,6 +264,69 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can get Bytes decoded transactions_%p`, + async (protocol) => { + client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new Transaction(); + const expectedRes = await encodedTransactionTest(transaction); + transaction.select(0); + const result = await client.exec(transaction, Decoder.Bytes); + expectedRes.push(["select(0)", "OK"]); + + validateTransactionResponse(result, expectedRes); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can send transaction with default string decoder_%p`, + async (protocol) => { + const clientConfig = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + clientConfig.defaultDecoder = Decoder.String; + client = await GlideClient.createClient(clientConfig); + expect(await client.select(0)).toEqual("OK"); + const transaction = new Transaction(); + const expectedRes = await encodableTransactionTest( + transaction, + "value", + ); + transaction.select(0); + const result = await client.exec(transaction); + expectedRes.push(["select(0)", "OK"]); + + validateTransactionResponse(result, expectedRes); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can send transaction with default bytes decoder_%p`, + async (protocol) => { + const clientConfig = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + clientConfig.defaultDecoder = Decoder.Bytes; + client = await GlideClient.createClient(clientConfig); + expect(await client.select(0)).toEqual("OK"); + const transaction = new Transaction(); + const valueEncoded = Buffer.from("value"); + const expectedRes = await encodableTransactionTest( + transaction, + valueEncoded, + ); + transaction.select(0); + const result = await client.exec(transaction); + expectedRes.push(["select(0)", "OK"]); + + validateTransactionResponse(result, expectedRes); + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "can return null on WATCH transaction failures", async (protocol) => { @@ -364,32 +471,22 @@ describe("GlideClient", () => { ); const result = await client.lolwut(); - expect(intoString(result)).toEqual( - expect.stringContaining("Redis ver. "), - ); + expect(result).toEqual(expect.stringContaining("Redis ver. ")); const result2 = await client.lolwut({ parameters: [] }); - expect(intoString(result2)).toEqual( - expect.stringContaining("Redis ver. "), - ); + expect(result2).toEqual(expect.stringContaining("Redis ver. ")); const result3 = await client.lolwut({ parameters: [50, 20] }); - expect(intoString(result3)).toEqual( - expect.stringContaining("Redis ver. "), - ); + expect(result3).toEqual(expect.stringContaining("Redis ver. ")); const result4 = await client.lolwut({ version: 6 }); - expect(intoString(result4)).toEqual( - expect.stringContaining("Redis ver. "), - ); + expect(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. "), - ); + expect(result5).toEqual(expect.stringContaining("Redis ver. ")); // transaction tests const transaction = new Transaction(); @@ -401,7 +498,7 @@ describe("GlideClient", () => { if (results) { for (const element of results) { - expect(intoString(element)).toEqual( + expect(element).toEqual( expect.stringContaining("Redis ver. "), ); } diff --git a/node/tests/GlideClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts index ed5fc35d92..de3af5e4d4 100644 --- a/node/tests/GlideClientInternals.test.ts +++ b/node/tests/GlideClientInternals.test.ts @@ -22,6 +22,7 @@ import { BaseClientConfiguration, ClosingError, ClusterTransaction, + Decoder, GlideClient, GlideClientConfiguration, GlideClusterClient, @@ -37,7 +38,7 @@ import { connection_request, response, } from "../src/ProtobufMessage"; -import { convertStringArrayToBuffer, intoString } from "./TestUtilities"; +import { convertStringArrayToBuffer } from "./TestUtilities"; const { RequestType, CommandRequest } = command_request; beforeAll(() => { @@ -308,8 +309,9 @@ describe("SocketConnectionInternals", () => { }, ); }); - const result = await connection.get("foo"); - expect(intoString(result)).toEqual(intoString(expected)); + const result = await connection.get("foo", Decoder.String); + console.log(result); + expect(result).toEqual(expected); }); }; @@ -384,7 +386,9 @@ describe("SocketConnectionInternals", () => { type: "primarySlotKey", key: "key", }; - const result = await connection.exec(transaction, slotKey); + const result = await connection.exec(transaction, { + route: slotKey, + }); expect(result).toBe("OK"); }); }); @@ -412,10 +416,10 @@ describe("SocketConnectionInternals", () => { }); const transaction = new ClusterTransaction(); transaction.info([InfoOptions.Server]); - const result = await connection.exec(transaction, "randomNode"); - expect(intoString(result)).toEqual( - expect.stringContaining("# Server"), - ); + const result = await connection.exec(transaction, { + route: "randomNode", + }); + expect(result).toEqual(expect.stringContaining("# Server")); }); }); @@ -701,14 +705,13 @@ describe("SocketConnectionInternals", () => { }); const result1 = await connection.customCommand( ["SET", "foo", "bar"], - route1, + { route: route1 }, ); expect(result1).toBeNull(); - const result2 = await connection.customCommand( - ["GET", "foo"], - route2, - ); + const result2 = await connection.customCommand(["GET", "foo"], { + route: route2, + }); expect(result2).toBeNull(); }); }); diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index db4b9f3282..fbb6a58d51 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from "uuid"; import { BitwiseOperation, ClusterTransaction, + Decoder, FunctionListResponse, GlideClusterClient, InfoOptions, @@ -113,9 +114,7 @@ describe("GlideClusterClient", () => { const info_server = getFirstResult( await client.info([InfoOptions.Server]), ); - expect(intoString(info_server)).toEqual( - expect.stringContaining("# Server"), - ); + expect(info_server).toEqual(expect.stringContaining("# Server")); const infoReplicationValues = Object.values( await client.info([InfoOptions.Replication]), @@ -141,12 +140,8 @@ describe("GlideClusterClient", () => { [InfoOptions.Server], "randomNode", ); - expect(intoString(result)).toEqual( - expect.stringContaining("# Server"), - ); - expect(intoString(result)).toEqual( - expect.not.stringContaining("# Errorstats"), - ); + expect(result).toEqual(expect.stringContaining("# Server")); + expect(result).toEqual(expect.not.stringContaining("# Errorstats")); }, TIMEOUT, ); @@ -169,10 +164,9 @@ describe("GlideClusterClient", () => { ); const result = cleanResult( intoString( - await client.customCommand( - ["cluster", "nodes"], - "randomNode", - ), + await client.customCommand(["cluster", "nodes"], { + route: "randomNode", + }), ), ); @@ -186,8 +180,10 @@ describe("GlideClusterClient", () => { const secondResult = cleanResult( intoString( await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host, + route: { + type: "routeByAddress", + host, + }, }), ), ); @@ -200,9 +196,11 @@ describe("GlideClusterClient", () => { const thirdResult = cleanResult( intoString( await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host: host2, - port: Number(port), + route: { + type: "routeByAddress", + host: host2, + port: Number(port), + }, }), ), ); @@ -228,6 +226,44 @@ describe("GlideClusterClient", () => { TIMEOUT, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `dump and restore custom command_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = "key"; + const value = "value"; + const valueEncoded = Buffer.from(value); + expect(await client.set(key, value)).toEqual("OK"); + // Since DUMP gets binary results, we cannot use the default decoder (string) here, so we expected to get an error. + // TODO: fix custom command with unmatch decoder to return an error: https://github.com/valkey-io/valkey-glide/issues/2119 + // expect(await client.customCommand(["DUMP", key])).toThrowError(); + const dumpResult = await client.customCommand(["DUMP", key], { + decoder: Decoder.Bytes, + }); + expect(await client.del([key])).toEqual(1); + + if (dumpResult instanceof Buffer) { + // check the delete + expect(await client.get(key)).toEqual(null); + expect( + await client.customCommand( + ["RESTORE", key, "0", dumpResult], + { decoder: Decoder.Bytes }, + ), + ).toEqual("OK"); + // check the restore + expect(await client.get(key)).toEqual(value); + expect(await client.get(key, Decoder.Bytes)).toEqual( + valueEncoded, + ); + } + }, + TIMEOUT, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set transactions test_%p`, async (protocol) => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 47c6e1cbb6..6b9d57142d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -22,6 +22,7 @@ import { ClosingError, ClusterTransaction, ConditionalChange, + Decoder, ExpireOptions, FlushMode, GeoUnit, @@ -1256,6 +1257,33 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `encoder test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const value = uuidv4(); + const valueEncoded = Buffer.from(value); + + expect(await client.set(key, value)).toEqual("OK"); + expect(await client.get(key)).toEqual(value); + expect(await client.get(key, Decoder.Bytes)).toEqual( + valueEncoded, + ); + expect(await client.get(key, Decoder.String)).toEqual(value); + + // Setting the encoded value. Should behave as the previous test since the default is String decoding. + expect(await client.set(key, valueEncoded)).toEqual("OK"); + expect(await client.get(key)).toEqual(value); + expect(await client.get(key, Decoder.Bytes)).toEqual( + valueEncoded, + ); + expect(await client.get(key, Decoder.String)).toEqual(value); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `hdel multiple existing fields, an non existing field and an non existing key_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 9660d06809..97b120144e 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -477,6 +477,76 @@ export function validateTransactionResponse( } } +/** + * Populates a transaction with commands to test the decodable commands with various default decoders. + * @param baseTransaction - A transaction. + * @param valueEncodedResponse - Represents the encoded response of "value" to compare + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ +export async function encodableTransactionTest( + baseTransaction: Transaction | ClusterTransaction, + valueEncodedResponse: ReturnType, +): Promise<[string, ReturnType][]> { + const key = "{key}" + uuidv4(); // string + const value = "value"; + // array of tuples - first element is test name/description, second - expected return value + const responseData: [string, ReturnType][] = []; + + baseTransaction.set(key, value); + responseData.push(["set(key, value)", "OK"]); + baseTransaction.get(key); + responseData.push(["get(key)", valueEncodedResponse]); + + return responseData; +} + +/** + * Populates a transaction with commands to test the decoded response. + * @param baseTransaction - A transaction. + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ +export async function encodedTransactionTest( + baseTransaction: Transaction | ClusterTransaction, +): Promise<[string, ReturnType][]> { + const key1 = "{key}" + uuidv4(); // string + const key2 = "{key}" + uuidv4(); // string + const key = "dumpKey"; + const dumpResult = Buffer.from([ + 0, 5, 118, 97, 108, 117, 101, 11, 0, 232, 41, 124, 75, 60, 53, 114, 231, + ]); + const value = "value"; + const valueEncoded = Buffer.from(value); + // array of tuples - first element is test name/description, second - expected return value + const responseData: [string, ReturnType][] = []; + + baseTransaction.set(key1, value); + responseData.push(["set(key1, value)", "OK"]); + baseTransaction.set(key2, value); + responseData.push(["set(key2, value)", "OK"]); + baseTransaction.get(key1); + responseData.push(["get(key1)", valueEncoded]); + baseTransaction.get(key2); + responseData.push(["get(key2)", valueEncoded]); + + baseTransaction.set(key, value); + responseData.push(["set(key, value)", "OK"]); + baseTransaction.customCommand(["DUMP", key]); + responseData.push(['customCommand(["DUMP", key])', dumpResult]); + baseTransaction.del([key]); + responseData.push(["del(key)", 1]); + baseTransaction.get(key); + responseData.push(["get(key)", null]); + baseTransaction.customCommand(["RESTORE", key, "0", dumpResult]); + responseData.push([ + 'customCommand(["RESTORE", key, "0", dumpResult])', + "OK", + ]); + baseTransaction.get(key); + responseData.push(["get(key)", valueEncoded]); + + return responseData; +} + /** * Populates a transaction with commands to test. * @param baseTransaction - A transaction. From 73a9a787d505f9bfb6da19c935d196c819569392 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 12 Aug 2024 09:44:09 -0700 Subject: [PATCH 164/236] Node: Add `XAUTOCLAIM` command. (#2108) * Add `XAUTOCLAIM` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 119 ++++++++++++++++++++++++++++++++ node/src/Commands.ts | 22 ++++++ node/src/Transaction.ts | 83 +++++++++++++++++++++++ node/tests/SharedTests.ts | 132 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 55 +++++++++------ 6 files changed, 390 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4531e039ad..9cca05bfb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) * Node: Added XPENDING commands ([#2085](https://github.com/valkey-io/valkey-glide/pull/2085)) * Node: Added XINFO CONSUMERS command ([#2093](https://github.com/valkey-io/valkey-glide/pull/2093)) * Node: Added HRANDFIELD command ([#2096](https://github.com/valkey-io/valkey-glide/pull/2096)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4ff9b6f9b9..d7b5af1a7d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -162,6 +162,7 @@ import { createUnlink, createWatch, createXAdd, + createXAutoClaim, createXClaim, createXDel, createXGroupCreate, @@ -4153,6 +4154,124 @@ export class BaseClient { ); } + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * See https://valkey.io/commands/xautoclaim/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param start - Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count - (Optional) Limits the number of claimed entries to the specified value. + * @returns A `tuple` containing the following elements: + * - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is + * equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if + * the entire stream was scanned. + * - A `Record` of the claimed entries. + * - If you are using Valkey 7.0.0 or above, the response list will also include a list containing + * the message IDs that were in the Pending Entries List but no longer exist in the stream. + * These IDs are deleted from the Pending Entries List. + * + * @example + * ```typescript + * const result = await client.xautoclaim("myStream", "myGroup", "myConsumer", 42, "0-0", 25); + * console.log(result); // Output: + * // [ + * // "1609338788321-0", // value to be used as `start` argument + * // // for the next `xautoclaim` call + * // { + * // "1609338752495-0": [ // claimed entries + * // ["field 1", "value 1"], + * // ["field 2", "value 2"] + * // ] + * // }, + * // [ + * // "1594324506465-0", // array of IDs of deleted messages, + * // "1594568784150-0" // included in the response only on valkey 7.0.0 and above + * // ] + * // ] + * ``` + */ + public async xautoclaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + count?: number, + ): Promise<[string, Record, string[]?]> { + return this.createWritePromise( + createXAutoClaim(key, group, consumer, minIdleTime, start, count), + ); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * See https://valkey.io/commands/xautoclaim/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param start - Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count - (Optional) Limits the number of claimed entries to the specified value. + * @returns An `array` containing the following elements: + * - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is + * equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if + * the entire stream was scanned. + * - A list of the IDs for the claimed entries. + * - If you are using Valkey 7.0.0 or above, the response list will also include a list containing + * the message IDs that were in the Pending Entries List but no longer exist in the stream. + * These IDs are deleted from the Pending Entries List. + * + * @example + * ```typescript + * const result = await client.xautoclaim("myStream", "myGroup", "myConsumer", 42, "0-0", 25); + * console.log(result); // Output: + * // [ + * // "1609338788321-0", // value to be used as `start` argument + * // // for the next `xautoclaim` call + * // [ + * // "1609338752495-0", // claimed entries + * // "1609338752495-1", + * // ], + * // [ + * // "1594324506465-0", // array of IDs of deleted messages, + * // "1594568784150-0" // included in the response only on valkey 7.0.0 and above + * // ] + * // ] + * ``` + */ + public async xautoclaimJustId( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + count?: number, + ): Promise<[string, string[], string[]?]> { + return this.createWritePromise( + createXAutoClaim( + key, + group, + consumer, + minIdleTime, + start, + count, + true, + ), + ); + } + /** * Changes the ownership of a pending message. This function returns an `array` with * only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 584ffb0a60..c5c8578c21 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2545,6 +2545,28 @@ export function createXClaim( return createCommand(RequestType.XClaim, args); } +/** @internal */ +export function createXAutoClaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + count?: number, + justId?: boolean, +): command_request.Command { + const args = [ + key, + group, + consumer, + minIdleTime.toString(), + start.toString(), + ]; + if (count !== undefined) args.push("COUNT", count.toString()); + if (justId) args.push("JUSTID"); + return createCommand(RequestType.XAutoClaim, args); +} + /** * Optional arguments for {@link BaseClient.xgroupCreate|xgroupCreate}. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 65266f1109..c044f9eeff 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -198,6 +198,7 @@ import { createType, createUnlink, createXAdd, + createXAutoClaim, createXClaim, createXDel, createXGroupCreate, @@ -2421,6 +2422,88 @@ export class BaseTransaction> { ); } + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * See https://valkey.io/commands/xautoclaim/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param start - Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count - (Optional) Limits the number of claimed entries to the specified value. + * + * Command Response - An `array` containing the following elements: + * - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is + * equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if + * the entire stream was scanned. + * - A mapping of the claimed entries. + * - If you are using Valkey 7.0.0 or above, the response list will also include a list containing + * the message IDs that were in the Pending Entries List but no longer exist in the stream. + * These IDs are deleted from the Pending Entries List. + */ + public xautoclaim( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + count?: number, + ): T { + return this.addAndReturn( + createXAutoClaim(key, group, consumer, minIdleTime, start, count), + ); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * See https://valkey.io/commands/xautoclaim/ for more details. + * + * since Valkey version 6.2.0. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param minIdleTime - The minimum idle time for the message to be claimed. + * @param start - Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count - (Optional) Limits the number of claimed entries to the specified value. + * + * Command Response - An `array` containing the following elements: + * - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is + * equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if + * the entire stream was scanned. + * - A list of the IDs for the claimed entries. + * - If you are using Valkey 7.0.0 or above, the response list will also include a list containing + * the message IDs that were in the Pending Entries List but no longer exist in the stream. + * These IDs are deleted from the Pending Entries List. + */ + public xautoclaimJustId( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + count?: number, + ): T { + return this.addAndReturn( + createXAutoClaim( + key, + group, + consumer, + minIdleTime, + start, + count, + true, + ), + ); + } + /** * Creates a new consumer group uniquely identified by `groupname` for the stream * stored at `key`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 6b9d57142d..ab7ae4fef0 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -7920,6 +7920,138 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xautoclaim test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = uuidv4(); + const group = uuidv4(); + + expect( + await client.xgroupCreate(key, group, "0", { + mkStream: true, + }), + ).toEqual("OK"); + expect( + await client.xgroupCreateConsumer(key, group, "consumer"), + ).toEqual(true); + + expect( + await client.xadd( + key, + [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + { id: "0-1" }, + ), + ).toEqual("0-1"); + expect( + await client.xadd( + key, + [["entry2_field1", "entry2_value1"]], + { id: "0-2" }, + ), + ).toEqual("0-2"); + + expect( + await client.customCommand([ + "xreadgroup", + "group", + group, + "consumer", + "STREAMS", + key, + ">", + ]), + ).toEqual({ + [key]: { + "0-1": [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + "0-2": [["entry2_field1", "entry2_value1"]], + }, + }); + + let result = await client.xautoclaim( + key, + group, + "consumer", + 0, + "0-0", + 1, + ); + let expected: typeof result = [ + "0-2", + { + "0-1": [ + ["entry1_field1", "entry1_value1"], + ["entry1_field2", "entry1_value2"], + ], + }, + ]; + if (!cluster.checkIfServerVersionLessThan("7.0.0")) + expected.push([]); + expect(result).toEqual(expected); + + let result2 = await client.xautoclaimJustId( + key, + group, + "consumer", + 0, + "0-0", + ); + let expected2: typeof result2 = ["0-0", ["0-1", "0-2"]]; + if (!cluster.checkIfServerVersionLessThan("7.0.0")) + expected2.push([]); + expect(result2).toEqual(expected2); + + // add one more entry + expect( + await client.xadd( + key, + [["entry3_field1", "entry3_value1"]], + { id: "0-3" }, + ), + ).toEqual("0-3"); + + // incorrect IDs - response is empty + result = await client.xautoclaim( + key, + group, + "consumer", + 0, + "5-0", + ); + expected = ["0-0", {}]; + if (!cluster.checkIfServerVersionLessThan("7.0.0")) + expected.push([]); + expect(result).toEqual(expected); + + result2 = await client.xautoclaimJustId( + key, + group, + "consumer", + 0, + "5-0", + ); + expected2 = ["0-0", []]; + if (!cluster.checkIfServerVersionLessThan("7.0.0")) + expected2.push([]); + expect(result2).toEqual(expected2); + + // key exists, but it is not a stream + const stringKey = uuidv4(); + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xautoclaim(stringKey, "_", "_", 0, "_"), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lmpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 97b120144e..e081fb0ab8 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1035,28 +1035,28 @@ export async function transactionTest( // key9 has one entry here: {"0-2":[["field","value2"]]} - baseTransaction.xgroupCreateConsumer(key9, groupName1, "consumer1"); + baseTransaction.xgroupCreateConsumer(key9, groupName1, consumer); responseData.push([ - "xgroupCreateConsumer(key9, groupName1, consumer1)", + "xgroupCreateConsumer(key9, groupName1, consumer)", true, ]); baseTransaction.customCommand([ "xreadgroup", "group", groupName1, - "consumer1", + consumer, "STREAMS", key9, ">", ]); responseData.push([ - 'xreadgroup(groupName1, "consumer1", key9, >)', + "xreadgroup(groupName1, consumer, key9, >)", { [key9]: { "0-2": [["field", "value2"]] } }, ]); baseTransaction.xpending(key9, groupName1); responseData.push([ "xpending(key9, groupName1)", - [1, "0-2", "0-2", [["consumer1", "1"]]], + [1, "0-2", "0-2", [[consumer, "1"]]], ]); baseTransaction.xpendingWithOptions(key9, groupName1, { start: InfScoreBoundary.NegativeInfinity, @@ -1064,44 +1064,55 @@ export async function transactionTest( count: 10, }); responseData.push([ - "xpending(key9, groupName1, -, +, 10)", - [["0-2", "consumer1", 0, 1]], + "xpendingWithOptions(key9, groupName1, -, +, 10)", + [["0-2", consumer, 0, 1]], ]); - baseTransaction.xclaim(key9, groupName1, "consumer1", 0, ["0-2"]); + baseTransaction.xclaim(key9, groupName1, consumer, 0, ["0-2"]); responseData.push([ - 'xclaim(key9, groupName1, "consumer1", 0, ["0-2"])', + 'xclaim(key9, groupName1, consumer, 0, ["0-2"])', { "0-2": [["field", "value2"]] }, ]); - baseTransaction.xclaim(key9, groupName1, "consumer1", 0, ["0-2"], { + baseTransaction.xclaim(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0, }); responseData.push([ - 'xclaim(key9, groupName1, "consumer1", 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', + 'xclaim(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', { "0-2": [["field", "value2"]] }, ]); - baseTransaction.xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"]); + baseTransaction.xclaimJustId(key9, groupName1, consumer, 0, ["0-2"]); responseData.push([ - 'xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"])', + 'xclaimJustId(key9, groupName1, consumer, 0, ["0-2"])', ["0-2"], ]); - baseTransaction.xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"], { + baseTransaction.xclaimJustId(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0, }); responseData.push([ - 'xclaimJustId(key9, groupName1, "consumer1", 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', + 'xclaimJustId(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', ["0-2"], ]); - baseTransaction.xgroupCreateConsumer(key9, groupName1, consumer); - responseData.push([ - "xgroupCreateConsumer(key9, groupName1, consumer)", - true, - ]); + + if (gte(version, "6.2.0")) { + baseTransaction.xautoclaim(key9, groupName1, consumer, 0, "0-0", 1); + responseData.push([ + 'xautoclaim(key9, groupName1, consumer, 0, "0-0", 1)', + gte(version, "7.0.0") + ? ["0-0", { "0-2": [["field", "value2"]] }, []] + : ["0-0", { "0-2": [["field", "value2"]] }], + ]); + baseTransaction.xautoclaimJustId(key9, groupName1, consumer, 0, "0-0"); + responseData.push([ + 'xautoclaimJustId(key9, groupName1, consumer, 0, "0-0")', + gte(version, "7.0.0") ? ["0-0", ["0-2"], []] : ["0-0", ["0-2"]], + ]); + } + baseTransaction.xgroupDelConsumer(key9, groupName1, consumer); - responseData.push(["xgroupDelConsumer(key9, groupName1, consumer)", 0]); + responseData.push(["xgroupDelConsumer(key9, groupName1, consumer)", 1]); baseTransaction.xgroupDestroy(key9, groupName1); responseData.push(["xgroupDestroy(key9, groupName1)", true]); baseTransaction.xgroupDestroy(key9, groupName2); @@ -1140,7 +1151,7 @@ export async function transactionTest( baseTransaction.bitpos(key17, 1); responseData.push(["bitpos(key17, 1)", 1]); - if (gte("6.0.0", version)) { + if (gte(version, "6.0.0")) { baseTransaction.bitfieldReadOnly(key17, [ new BitFieldGet(new SignedEncoding(5), new BitOffset(3)), ]); From dcd31b9857ad7f1da81b1d82aae28e06cdf1c18c Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:19:47 -0700 Subject: [PATCH 165/236] Node: add HSCAN (#2098) add HSCAN --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 46 +++++++++ node/src/Commands.ts | 42 ++++++-- node/src/Transaction.ts | 20 ++++ node/tests/SharedTests.ts | 193 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 7 ++ 6 files changed, 302 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cca05bfb2..33e6b49599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ #### Changes * Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) * Node: Added XPENDING commands ([#2085](https://github.com/valkey-io/valkey-glide/pull/2085)) +* Node: Added HSCAN command ([#2098](https://github.com/valkey-io/valkey-glide/pull/2098/)) * Node: Added XINFO CONSUMERS command ([#2093](https://github.com/valkey-io/valkey-glide/pull/2093)) * Node: Added HRANDFIELD command ([#2096](https://github.com/valkey-io/valkey-glide/pull/2096)) * Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d7b5af1a7d..31c2d65544 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -92,6 +92,7 @@ import { createHLen, createHMGet, createHRandField, + createHScan, createHSet, createHSetNX, createHStrlen, @@ -1712,6 +1713,51 @@ export class BaseClient { return this.createWritePromise(createHRandField(key)); } + /** + * Iterates incrementally over a hash. + * + * See https://valkey.io/commands/hscan for more details. + * + * @param key - The key of the set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. + * @param options - (Optional) The {@link BaseScanOptions}. + * @returns An array of the `cursor` and the subset of the hash held by `key`. + * The first element is always the `cursor` for the next iteration of results. `"0"` will be the `cursor` + * returned on the last iteration of the hash. The second element is always an array of the subset of the + * hash held in `key`. The array in the second element is always a flattened series of string pairs, + * where the value is at even indices and the value is at odd indices. + * + * @example + * ```typescript + * // Assume "key" contains a hash with multiple members + * let newCursor = "0"; + * let result = []; + * do { + * result = await client.hscan(key1, newCursor, { + * match: "*", + * count: 3, + * }); + * newCursor = result[0]; + * console.log("Cursor: ", newCursor); + * console.log("Members: ", result[1]); + * } while (newCursor !== "0"); + * // The output of the code above is something similar to: + * // Cursor: 31 + * // Members: ['field 79', 'value 79', 'field 20', 'value 20', 'field 115', 'value 115'] + * // Cursor: 39 + * // Members: ['field 63', 'value 63', 'field 293', 'value 293', 'field 162', 'value 162'] + * // Cursor: 0 + * // Members: ['value 55', '55', 'value 24', '24', 'value 90', '90', 'value 113', '113'] + * ``` + */ + public async hscan( + key: string, + cursor: string, + options?: BaseScanOptions, + ): Promise<[string, string[]]> { + return this.createWritePromise(createHScan(key, cursor, options)); + } + /** * Retrieves up to `count` random field names from the hash value stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index c5c8578c21..30dab3bcd3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3362,6 +3362,23 @@ export function createHRandField( return createCommand(RequestType.HRandField, args); } +/** + * @internal + */ +export function createHScan( + key: string, + cursor: string, + options?: BaseScanOptions, +): command_request.Command { + let args: string[] = [key, cursor]; + + if (options) { + args = args.concat(convertBaseScanOptionsToArgsArray(options)); + } + + return createCommand(RequestType.HScan, args); +} + /** * @internal */ @@ -3456,6 +3473,23 @@ export type BaseScanOptions = { readonly count?: number; }; +/** + * @internal + */ +function convertBaseScanOptionsToArgsArray(options: BaseScanOptions): string[] { + const args: string[] = []; + + if (options.match) { + args.push("MATCH", options.match); + } + + if (options.count !== undefined) { + args.push("COUNT", options.count.toString()); + } + + return args; +} + /** * @internal */ @@ -3467,13 +3501,7 @@ export function createZScan( let args: string[] = [key, cursor]; if (options) { - if (options.match) { - args = args.concat("MATCH", options.match); - } - - if (options.count !== undefined) { - args = args.concat("COUNT", options.count.toString()); - } + args = args.concat(convertBaseScanOptionsToArgsArray(options)); } return createCommand(RequestType.ZScan, args); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c044f9eeff..2933e5bac7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -116,6 +116,7 @@ import { createHLen, createHMGet, createHRandField, + createHScan, createHSet, createHSetNX, createHStrlen, @@ -866,6 +867,25 @@ export class BaseTransaction> { return this.addAndReturn(createHRandField(key)); } + /** + * Iterates incrementally over a hash. + * + * See https://valkey.io/commands/hscan for more details. + * + * @param key - The key of the set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. + * @param options - (Optional) The {@link BaseScanOptions}. + * + * Command Response - An array of the `cursor` and the subset of the hash held by `key`. + * The first element is always the `cursor` for the next iteration of results. `"0"` will be the `cursor` + * returned on the last iteration of the hash. The second element is always an array of the subset of the + * hash held in `key`. The array in the second element is always a flattened series of string pairs, + * where the value is at even indices and the value is at odd indices. + */ + public hscan(key: string, cursor: string, options?: BaseScanOptions): T { + return this.addAndReturn(createHScan(key, cursor, options)); + } + /** * Retrieves up to `count` random field names from the hash value stored at `key`. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ab7ae4fef0..151ac96d3e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1257,6 +1257,199 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `hscan test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const initialCursor = "0"; + const defaultCount = 20; + const resultCursorIndex = 0; + const resultCollectionIndex = 1; + + // Setup test data - use a large number of entries to force an iterative cursor. + const numberMap: Record = {}; + + for (let i = 0; i < 50000; i++) { + numberMap[i.toString()] = "num" + i; + } + + const charMembers = ["a", "b", "c", "d", "e"]; + const charMap: Record = {}; + + for (let i = 0; i < charMembers.length; i++) { + charMap[charMembers[i]] = i.toString(); + } + + // Result contains the whole set + expect(await client.hset(key1, charMap)).toEqual( + charMembers.length, + ); + let result = await client.hscan(key1, initialCursor); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex].length).toEqual( + Object.keys(charMap).length * 2, // Length includes the score which is twice the map size + ); + + const resultArray = result[resultCollectionIndex]; + const resultKeys = []; + const resultValues: string[] = []; + + for (let i = 0; i < resultArray.length; i += 2) { + resultKeys.push(resultArray[i]); + resultValues.push(resultArray[i + 1]); + } + + // Verify if all keys from charMap are in resultKeys + const allKeysIncluded = resultKeys.every( + (key) => key in charMap, + ); + expect(allKeysIncluded).toEqual(true); + + const allValuesIncluded = Object.values(charMap).every( + (value) => value in resultValues, + ); + expect(allValuesIncluded).toEqual(true); + + // Test hscan with match + result = await client.hscan(key1, initialCursor, { + match: "a", + }); + + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual(["a", "0"]); + + // Set up testing data with the numberMap set to be used for the next set test keys and test results. + expect(await client.hset(key1, numberMap)).toEqual( + Object.keys(numberMap).length, + ); + + let resultCursor = initialCursor; + const secondResultAllKeys: string[] = []; + const secondResultAllValues: string[] = []; + let isFirstLoop = true; + + do { + result = await client.hscan(key1, resultCursor); + resultCursor = result[resultCursorIndex].toString(); + const resultEntry = result[resultCollectionIndex]; + + for (let i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.push(resultEntry[i]); + secondResultAllValues.push(resultEntry[i + 1]); + } + + if (isFirstLoop) { + expect(resultCursor).not.toBe("0"); + isFirstLoop = false; + } else if (resultCursor === initialCursor) { + break; + } + + // Scan with result cursor has a different set + const secondResult = await client.hscan(key1, resultCursor); + const newResultCursor = + secondResult[resultCursorIndex].toString(); + expect(resultCursor).not.toBe(newResultCursor); + resultCursor = newResultCursor; + const secondResultEntry = + secondResult[resultCollectionIndex]; + + expect(result[resultCollectionIndex]).not.toBe( + secondResult[resultCollectionIndex], + ); + + for (let i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.push(secondResultEntry[i]); + secondResultAllValues.push(secondResultEntry[i + 1]); + } + } while (resultCursor != initialCursor); // 0 is returned for the cursor of the last iteration. + + // Verify all data is found in hscan + const allSecondResultKeys = Object.keys(numberMap).every( + (key) => key in secondResultAllKeys, + ); + expect(allSecondResultKeys).toEqual(true); + + const allSecondResultValues = Object.keys(numberMap).every( + (value) => value in secondResultAllValues, + ); + expect(allSecondResultValues).toEqual(true); + + // Test match pattern + result = await client.hscan(key1, initialCursor, { + match: "*", + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(defaultCount); + + // Test count + result = await client.hscan(key1, initialCursor, { + count: 25, + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(25); + + // Test count with match returns a non-empty list + result = await client.hscan(key1, initialCursor, { + match: "1*", + count: 30, + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect(result[resultCollectionIndex].length).toBeGreaterThan(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `hscan empty set, negative cursor, negative count, and non-hash key exception tests`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const initialCursor = "0"; + const resultCursorIndex = 0; + const resultCollectionIndex = 1; + + // Empty set + let result = await client.hscan(key1, initialCursor); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + + // Negative cursor + result = await client.hscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + + // Exceptions + // Non-hash key + expect(await client.set(key2, "test")).toEqual("OK"); + await expect(client.hscan(key2, initialCursor)).rejects.toThrow( + RequestError, + ); + await expect( + client.hscan(key2, initialCursor, { + match: "test", + count: 20, + }), + ).rejects.toThrow(RequestError); + + // Negative count + await expect( + client.hscan(key2, initialCursor, { + count: -1, + }), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `encoder test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e081fb0ab8..dba0f69f17 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -657,6 +657,13 @@ export async function transactionTest( responseData.push(["del([key1])", 1]); baseTransaction.hset(key4, { [field]: value }); responseData.push(["hset(key4, { [field]: value })", 1]); + baseTransaction.hscan(key4, "0"); + responseData.push(['hscan(key4, "0")', ["0", [field, value]]]); + baseTransaction.hscan(key4, "0", { match: "*", count: 20 }); + responseData.push([ + 'hscan(key4, "0", {match: "*", count: 20})', + ["0", [field, value]], + ]); baseTransaction.hstrlen(key4, field); responseData.push(["hstrlen(key4, field)", value.length]); baseTransaction.hlen(key4); From 044c111232c966cd630af9260a1e266d60936cb4 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Tue, 13 Aug 2024 00:23:09 +0000 Subject: [PATCH 166/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 24 ++++++++++----------- java/THIRD_PARTY_LICENSES_JAVA | 24 ++++++++++----------- node/THIRD_PARTY_LICENSES_NODE | 32 ++++++++++++++-------------- python/THIRD_PARTY_LICENSES_PYTHON | 26 +++++++++++----------- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 3508f83266..baf1a730e1 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -4240,7 +4240,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: core-foundation-sys:0.8.7 The following copyrights and licenses were found in the source code of this package: @@ -11903,7 +11903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: js-sys:0.3.70 The following copyrights and licenses were found in the source code of this package: @@ -13582,7 +13582,7 @@ the following restrictions: ---- -Package: mio:1.0.1 +Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -14802,7 +14802,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.2 +Package: object:0.36.3 The following copyrights and licenses were found in the source code of this package: @@ -21689,7 +21689,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.204 +Package: serde:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -21918,7 +21918,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.204 +Package: serde_derive:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -23021,7 +23021,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.72 +Package: syn:2.0.74 The following copyrights and licenses were found in the source code of this package: @@ -27070,7 +27070,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.92 +Package: wasm-bindgen:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -27299,7 +27299,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.92 +Package: wasm-bindgen-backend:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -27528,7 +27528,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.92 +Package: wasm-bindgen-macro:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -27757,7 +27757,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.92 +Package: wasm-bindgen-macro-support:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -27986,7 +27986,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.92 +Package: wasm-bindgen-shared:0.2.93 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 bd1e88cb68..f4fcb68d47 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -4469,7 +4469,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: core-foundation-sys:0.8.7 The following copyrights and licenses were found in the source code of this package: @@ -12798,7 +12798,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: js-sys:0.3.70 The following copyrights and licenses were found in the source code of this package: @@ -14477,7 +14477,7 @@ the following restrictions: ---- -Package: mio:1.0.1 +Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -15697,7 +15697,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.2 +Package: object:0.36.3 The following copyrights and licenses were found in the source code of this package: @@ -22584,7 +22584,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.204 +Package: serde:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -22813,7 +22813,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.204 +Package: serde_derive:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -23916,7 +23916,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.72 +Package: syn:2.0.74 The following copyrights and licenses were found in the source code of this package: @@ -27965,7 +27965,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.92 +Package: wasm-bindgen:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -28194,7 +28194,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.92 +Package: wasm-bindgen-backend:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -28423,7 +28423,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.92 +Package: wasm-bindgen-macro:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -28652,7 +28652,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.92 +Package: wasm-bindgen-macro-support:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -28881,7 +28881,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.92 +Package: wasm-bindgen-shared:0.2.93 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 88edc5a8c0..28f95e8a6b 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -4317,7 +4317,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: core-foundation-sys:0.8.7 The following copyrights and licenses were found in the source code of this package: @@ -12417,7 +12417,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: js-sys:0.3.70 The following copyrights and licenses were found in the source code of this package: @@ -14114,7 +14114,7 @@ the following restrictions: ---- -Package: mio:1.0.1 +Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -14164,7 +14164,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi:2.16.8 +Package: napi:2.16.9 The following copyrights and licenses were found in the source code of this package: @@ -14214,7 +14214,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive:2.16.10 +Package: napi-derive:2.16.11 The following copyrights and licenses were found in the source code of this package: @@ -14239,7 +14239,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive-backend:1.0.72 +Package: napi-derive-backend:1.0.73 The following copyrights and licenses were found in the source code of this package: @@ -15459,7 +15459,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.2 +Package: object:0.36.3 The following copyrights and licenses were found in the source code of this package: @@ -23262,7 +23262,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.204 +Package: serde:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -23491,7 +23491,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.204 +Package: serde_derive:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -24594,7 +24594,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.72 +Package: syn:2.0.74 The following copyrights and licenses were found in the source code of this package: @@ -29330,7 +29330,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.92 +Package: wasm-bindgen:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -29559,7 +29559,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.92 +Package: wasm-bindgen-backend:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -29788,7 +29788,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.92 +Package: wasm-bindgen-macro:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -30017,7 +30017,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.92 +Package: wasm-bindgen-macro-support:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -30246,7 +30246,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.92 +Package: wasm-bindgen-shared:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.1.0 +Package: @types:node:22.2.0 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 97ecdd3b01..98761ba6c4 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -4240,7 +4240,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: core-foundation-sys:0.8.7 The following copyrights and licenses were found in the source code of this package: @@ -12569,7 +12569,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: js-sys:0.3.70 The following copyrights and licenses were found in the source code of this package: @@ -14273,7 +14273,7 @@ the following restrictions: ---- -Package: mio:1.0.1 +Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -15493,7 +15493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.36.2 +Package: object:0.36.3 The following copyrights and licenses were found in the source code of this package: @@ -23754,7 +23754,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.204 +Package: serde:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -23983,7 +23983,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.204 +Package: serde_derive:1.0.207 The following copyrights and licenses were found in the source code of this package: @@ -25086,7 +25086,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.72 +Package: syn:2.0.74 The following copyrights and licenses were found in the source code of this package: @@ -29588,7 +29588,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.92 +Package: wasm-bindgen:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -29817,7 +29817,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.92 +Package: wasm-bindgen-backend:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -30046,7 +30046,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.92 +Package: wasm-bindgen-macro:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -30275,7 +30275,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.92 +Package: wasm-bindgen-macro-support:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -30504,7 +30504,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.92 +Package: wasm-bindgen-shared:0.2.93 The following copyrights and licenses were found in the source code of this package: @@ -35832,7 +35832,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: google-auth:2.32.0 +Package: google-auth:2.33.0 The following copyrights and licenses were found in the source code of this package: From 3a206836aacbde8eee1f1d29e878d9399ab6d5a7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 13 Aug 2024 09:00:45 -0700 Subject: [PATCH 167/236] Node: Remove `LASTID` option from `XCLAIM` (#2106) Remove `LASTID` option. Signed-off-by: Yury-Fridlyand --- node/src/Commands.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 30dab3bcd3..468340c8ff 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2513,9 +2513,6 @@ export type StreamClaimOptions = { * otherwise the IDs of non-existing messages are ignored. */ isForce?: boolean; - - /** The last ID of the entry which should be claimed. */ - lastId?: string; }; /** @internal */ @@ -2538,7 +2535,6 @@ export function createXClaim( if (options.retryCount !== undefined) args.push("RETRYCOUNT", options.retryCount.toString()); if (options.isForce) args.push("FORCE"); - if (options.lastId) args.push("LASTID", options.lastId); } if (justId) args.push("JUSTID"); From a05332e600eab84b8fe9501221649c36c95540a7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 13 Aug 2024 14:05:27 -0700 Subject: [PATCH 168/236] Node: Add `FUNCTION KILL` command (#2114) * Add `FUNCTION KILL` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Guian Gumpac --- CHANGELOG.md | 1 + .../src/test/java/glide/TestUtilities.java | 4 +- node/src/Commands.ts | 5 + node/src/GlideClient.ts | 19 ++ node/src/GlideClusterClient.ts | 23 ++ node/tests/GlideClient.test.ts | 151 ++++++++++++ node/tests/GlideClusterClient.test.ts | 218 ++++++++++++++++-- node/tests/TestUtilities.ts | 51 ++++ 8 files changed, 448 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e6b49599..3e33086d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) * Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) * Node: Added XPENDING commands ([#2085](https://github.com/valkey-io/valkey-glide/pull/2085)) * Node: Added HSCAN command ([#2098](https://github.com/valkey-io/valkey-glide/pull/2098/)) diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 9fc3a2931b..97c5222ba7 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -323,8 +323,8 @@ public static GlideString generateLuaLibCodeBinary( } /** - * Create a lua lib with a RO function which runs an endless loop up to timeout sec.
- * Execution takes at least 5 sec regardless of the timeout configured.
+ * Create a lua lib with a function which runs an endless loop up to timeout sec.
+ * Execution takes at least 5 sec regardless of the timeout configured. */ public static String createLuaLibWithLongRunningFunction( String libName, String funcName, int timeout, boolean readOnly) { diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 468340c8ff..baaccb0d92 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2240,6 +2240,11 @@ export function createFunctionStats(): command_request.Command { return createCommand(RequestType.FunctionStats, []); } +/** @internal */ +export function createFunctionKill(): command_request.Command { + return createCommand(RequestType.FunctionKill, []); +} + /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index bf631b898d..4dfb69d567 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -34,6 +34,7 @@ import { createFlushDB, createFunctionDelete, createFunctionFlush, + createFunctionKill, createFunctionList, createFunctionLoad, createFunctionStats, @@ -634,6 +635,24 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionStats()); } + /** + * Kills a function that is currently executing. + * `FUNCTION KILL` terminates read-only functions only. + * + * See https://valkey.io/commands/function-kill/ for details. + * + * since Valkey version 7.0.0. + * + * @returns `OK` if function is terminated. Otherwise, throws an error. + * @example + * ```typescript + * await client.functionKill(); + * ``` + */ + public async functionKill(): Promise<"OK"> { + return this.createWritePromise(createFunctionKill()); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 4cf01a7073..479e6d87da 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -36,6 +36,7 @@ import { createFlushDB, createFunctionDelete, createFunctionFlush, + createFunctionKill, createFunctionList, createFunctionLoad, createFunctionStats, @@ -960,6 +961,28 @@ export class GlideClusterClient extends BaseClient { }); } + /** + * Kills a function that is currently executing. + * `FUNCTION KILL` terminates read-only functions only. + * + * See https://valkey.io/commands/function-kill/ for details. + * + * since Valkey version 7.0.0. + * + * @param route - (Optional) The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed to all primary nodes. + * @returns `OK` if function is terminated. Otherwise, throws an error. + * @example + * ```typescript + * await client.functionKill(); + * ``` + */ + public async functionKill(route?: Routes): Promise<"OK"> { + return this.createWritePromise(createFunctionKill(), { + route: toProtobufRoute(route), + }); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 656481bd53..48d1035603 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -28,6 +28,7 @@ import { checkFunctionListResponse, checkFunctionStatsResponse, convertStringArrayToBuffer, + createLuaLibWithLongRunningFunction, encodableTransactionTest, encodedTransactionTest, flushAndCloseClient, @@ -37,6 +38,7 @@ import { parseEndpoints, transactionTest, validateTransactionResponse, + waitForNotBusy, } from "./TestUtilities"; /* eslint-disable @typescript-eslint/no-var-requires */ @@ -830,6 +832,155 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function kill RO func %p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + 10000, + ); + const client = await GlideClient.createClient(config); + const testClient = await GlideClient.createClient(config); + + try { + const libName = "function_kill_no_write"; + const funcName = "deadlock_no_write"; + const code = createLuaLibWithLongRunningFunction( + libName, + funcName, + 6, + true, + ); + expect(await client.functionFlush()).toEqual("OK"); + // nothing to kill + await expect(client.functionKill()).rejects.toThrow(/notbusy/i); + + // load the lib + expect(await client.functionLoad(code, true)).toEqual(libName); + + try { + // call the function without await + const promise = testClient + .fcall(funcName, [], []) + .catch((e) => + expect((e as Error).message).toContain( + "Script killed", + ), + ); + + let killed = false; + let timeout = 4000; + await new Promise((resolve) => setTimeout(resolve, 1000)); + + while (timeout >= 0) { + try { + expect(await client.functionKill()).toEqual("OK"); + killed = true; + break; + } catch { + // do nothing + } + + await new Promise((resolve) => + setTimeout(resolve, 500), + ); + timeout -= 500; + } + + expect(killed).toBeTruthy(); + await promise; + } finally { + await waitForNotBusy(client); + } + } finally { + expect(await client.functionFlush()).toEqual("OK"); + testClient.close(); + client.close(); + } + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function kill RW func %p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + 10000, + ); + const client = await GlideClient.createClient(config); + const testClient = await GlideClient.createClient(config); + + try { + const libName = "function_kill_write"; + const key = libName; + const funcName = "deadlock_write"; + const code = createLuaLibWithLongRunningFunction( + libName, + funcName, + 6, + false, + ); + expect(await client.functionFlush()).toEqual("OK"); + // nothing to kill + await expect(client.functionKill()).rejects.toThrow(/notbusy/i); + + // load the lib + expect(await client.functionLoad(code, true)).toEqual(libName); + + let promise = null; + + try { + // call the function without await + promise = testClient.fcall(funcName, [key], []); + + let foundUnkillable = false; + let timeout = 4000; + await new Promise((resolve) => setTimeout(resolve, 1000)); + + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + await client.functionKill(); + } catch (err) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if ( + (err as Error).message + .toLowerCase() + .includes("unkillable") + ) { + foundUnkillable = true; + break; + } + } + + await new Promise((resolve) => + setTimeout(resolve, 500), + ); + timeout -= 500; + } + + expect(foundUnkillable).toBeTruthy(); + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. Wait for the function to complete (we cannot kill it) + expect(await promise).toContain("Timed out"); + } + } finally { + expect(await client.functionFlush()).toEqual("OK"); + testClient.close(); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", async (protocol) => { diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index fbb6a58d51..d76ff0310a 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -37,6 +37,7 @@ import { checkClusterResponse, checkFunctionListResponse, checkFunctionStatsResponse, + createLuaLibWithLongRunningFunction, flushAndCloseClient, generateLuaLibCode, getClientConfigurationOption, @@ -47,6 +48,7 @@ import { parseEndpoints, transactionTest, validateTransactionResponse, + waitForNotBusy, } from "./TestUtilities"; type Context = { client: GlideClusterClient; @@ -998,17 +1000,6 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { it( "function flush", async () => { @@ -1095,17 +1086,6 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { it( "function delete", async () => { @@ -1179,7 +1159,201 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + it( + "function kill with route", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + 10000, + ); + const client = + await GlideClusterClient.createClient(config); + const testClient = + await GlideClusterClient.createClient(config); + + try { + const libName = + "function_kill_no_write_with_route_" + + singleNodeRoute; + const funcName = + "deadlock_with_route_" + singleNodeRoute; + const code = + createLuaLibWithLongRunningFunction( + libName, + funcName, + 6, + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + expect(await client.functionFlush()).toEqual( + "OK", + ); + + // nothing to kill + await expect( + client.functionKill(route), + ).rejects.toThrow(/notbusy/i); + + // load the lib + expect( + await client.functionLoad( + code, + true, + route, + ), + ).toEqual(libName); + + try { + // call the function without await + const promise = testClient + .fcallWithRoute(funcName, [], route) + .catch((e) => + expect( + (e as Error).message, + ).toContain("Script killed"), + ); + + let killed = false; + let timeout = 4000; + await new Promise((resolve) => + setTimeout(resolve, 1000), + ); + + while (timeout >= 0) { + try { + expect( + await client.functionKill( + route, + ), + ).toEqual("OK"); + killed = true; + break; + } catch { + // do nothing + } + + await new Promise((resolve) => + setTimeout(resolve, 500), + ); + timeout -= 500; + } + + expect(killed).toBeTruthy(); + await promise; + } finally { + await waitForNotBusy(client); + } + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + testClient.close(); + } + }, + TIMEOUT, + ); + }, + ); + it( + "function kill key based write function", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + 10000, + ); + const client = + await GlideClusterClient.createClient(config); + const testClient = + await GlideClusterClient.createClient(config); + + try { + const libName = + "function_kill_key_based_write_function"; + const funcName = + "deadlock_write_function_with_key_based_route"; + const key = libName; + const code = createLuaLibWithLongRunningFunction( + libName, + funcName, + 6, + false, + ); + + const route: Routes = { + type: "primarySlotKey", + key: key, + }; + expect(await client.functionFlush()).toEqual("OK"); + + // nothing to kill + await expect( + client.functionKill(route), + ).rejects.toThrow(/notbusy/i); + + // load the lib + expect( + await client.functionLoad(code, true, route), + ).toEqual(libName); + + let promise = null; + + try { + // call the function without await + promise = testClient.fcall(funcName, [key], []); + + let foundUnkillable = false; + let timeout = 4000; + await new Promise((resolve) => + setTimeout(resolve, 1000), + ); + + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + await client.functionKill(route); + } catch (err) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if ( + (err as Error).message + .toLowerCase() + .includes("unkillable") + ) { + foundUnkillable = true; + break; + } + } + + await new Promise((resolve) => + setTimeout(resolve, 500), + ); + timeout -= 500; + } + + expect(foundUnkillable).toBeTruthy(); + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. Wait for the function to complete (we cannot kill it) + expect(await promise).toContain("Timed out"); + } + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + testClient.close(); + } }, + TIMEOUT, ); }, ); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index dba0f69f17..f1a8ade345 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -254,6 +254,57 @@ export function generateLuaLibCode( return code; } +/** + * Create a lua lib with a function which runs an endless loop up to timeout sec. + * Execution takes at least 5 sec regardless of the timeout configured. + */ +export function createLuaLibWithLongRunningFunction( + libName: string, + funcName: string, + timeout: number, + readOnly: boolean, +): string { + const code = + "#!lua name=$libName\n" + + "local function $libName_$funcName(keys, args)\n" + + " local started = tonumber(redis.pcall('time')[1])\n" + + // fun fact - redis does no write if 'no-writes' flag is set + " redis.pcall('set', keys[1], 42)\n" + + " while (true) do\n" + + " local now = tonumber(redis.pcall('time')[1])\n" + + " if now > started + $timeout then\n" + + " return 'Timed out $timeout sec'\n" + + " end\n" + + " end\n" + + " return 'OK'\n" + + "end\n" + + "redis.register_function{\n" + + "function_name='$funcName',\n" + + "callback=$libName_$funcName,\n" + + (readOnly ? "flags={ 'no-writes' }\n" : "") + + "}"; + return code + .replaceAll("$timeout", timeout.toString()) + .replaceAll("$funcName", funcName) + .replaceAll("$libName", libName); +} + +export async function waitForNotBusy(client: GlideClusterClient | GlideClient) { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest test to fail. + let isBusy = true; + + do { + try { + await client.functionKill(); + } catch (err) { + // should throw `notbusy` error, because the function should be killed before + if ((err as Error).message.toLowerCase().includes("notbusy")) { + isBusy = false; + } + } + } while (isBusy); +} + /** * Parses the command-line arguments passed to the Node.js process. * From eb92d07dd9797e57c5a9d326c1dca16bb4cbed46 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Tue, 13 Aug 2024 17:53:53 -0700 Subject: [PATCH 169/236] Node: added WAIT command (#2113) * Node: added WAIT command Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 23 +++++++++++++++++++++++ node/src/Commands.ts | 11 +++++++++++ node/src/Transaction.ts | 18 ++++++++++++++++++ node/tests/SharedTests.ts | 32 ++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 6 files changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e33086d73..7be482a0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) +* Node: Added WAIT command ([#2113](https://github.com/valkey-io/valkey-glide/pull/2113)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZRANGESTORE command ([#2068](https://github.com/valkey-io/valkey-glide/pull/2068)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 31c2d65544..d445b60497 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -161,6 +161,7 @@ import { createTouch, createType, createUnlink, + createWait, createWatch, createXAdd, createXAutoClaim, @@ -5546,6 +5547,28 @@ export class BaseClient { return this.createWritePromise(createWatch(keys)); } + /** + * Blocks the current client until all the previous write commands are successfully transferred and + * acknowledged by at least `numreplicas` of replicas. If `timeout` is reached, the command returns + * the number of replicas that were not yet reached. + * + * See https://valkey.io/commands/wait/ for more details. + * + * @param numreplicas - The number of replicas to reach. + * @param timeout - The timeout value specified in milliseconds. A value of 0 will block indefinitely. + * @returns The number of replicas reached by all the writes performed in the context of the current connection. + * + * @example + * ```typescript + * await client.set(key, value); + * let response = await client.wait(1, 1000); + * console.log(response); // Output: return 1 when a replica is reached or 0 if 1000ms is reached. + * ``` + */ + public async wait(numreplicas: number, timeout: number): Promise { + return this.createWritePromise(createWait(numreplicas, timeout)); + } + /** * Overwrites part of the string stored at `key`, starting at the specified `offset`, * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index baaccb0d92..47094c43b3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3452,6 +3452,17 @@ export function createUnWatch(): command_request.Command { return createCommand(RequestType.UnWatch, []); } +/** @internal */ +export function createWait( + numreplicas: number, + timeout: number, +): command_request.Command { + return createCommand(RequestType.Wait, [ + numreplicas.toString(), + timeout.toString(), + ]); +} + /** * This base class represents the common set of optional arguments for the SCAN family of commands. * Concrete implementations of this class are tied to specific SCAN commands (SCAN, HSCAN, SSCAN, diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2933e5bac7..b1441ea566 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -198,6 +198,7 @@ import { createTouch, createType, createUnlink, + createWait, createXAdd, createXAutoClaim, createXClaim, @@ -2774,6 +2775,23 @@ export class BaseTransaction> { return this.addAndReturn(createLolwut(options)); } + /** + * Blocks the current client until all the previous write commands are successfully transferred and + * acknowledged by at least `numreplicas` of replicas. If `timeout` is reached, the command returns + * the number of replicas that were not yet reached. + * + * See https://valkey.io/commands/wait/ for more details. + * + * @param numreplicas - The number of replicas to reach. + * @param timeout - The timeout value specified in milliseconds. A value of 0 will block indefinitely. + * + * Command Response - The number of replicas reached by all the writes performed in the context of the + * current connection. + */ + public wait(numreplicas: number, timeout: number): T { + return this.addAndReturn(createWait(numreplicas, timeout)); + } + /** * Invokes a previously loaded function. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 151ac96d3e..5c24fd6f78 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5633,6 +5633,38 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "wait test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const value1 = uuidv4(); + const value2 = uuidv4(); + + // assert that wait returns 0 under standalone and 1 under cluster mode. + expect(await client.set(key, value1)).toEqual("OK"); + + if (client instanceof GlideClusterClient) { + expect(await client.wait(1, 1000)).toBeGreaterThanOrEqual( + 1, + ); + } else { + expect(await client.wait(1, 1000)).toBeGreaterThanOrEqual( + 0, + ); + } + + // command should fail on a negative timeout value + await expect(client.wait(1, -1)).rejects.toThrow(RequestError); + + // ensure that command doesn't time out even if timeout > request timeout (250ms by default) + expect(await client.set(key, value2)).toEqual("OK"); + expect(await client.wait(100, 500)).toBeGreaterThanOrEqual(0); + }, protocol); + }, + config.timeout, + ); + // Set command tests async function setWithExpiryOptions(client: BaseClient) { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index f1a8ade345..25f9c77d24 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1518,5 +1518,7 @@ export async function transactionTest( responseData.push(["sortReadOnly(key21)", ["1", "2", "3"]]); } + baseTransaction.wait(1, 200); + responseData.push(["wait(1, 200)", 1]); return responseData; } From 27342ab0c67624b2caeb0e4a32d8facf73be9535 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:46:14 -0700 Subject: [PATCH 170/236] Node: Add command GETEX (#2107) * Node: Add command GETEX Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 30 ++++++- node/src/Commands.ts | 112 +++++++++++++++--------- node/src/Transaction.ts | 23 +++++ node/tests/GlideClientInternals.test.ts | 3 +- node/tests/SharedTests.ts | 80 +++++++++++++---- node/tests/TestUtilities.ts | 16 +++- 8 files changed, 206 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be482a0cd..f54f490f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ * Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) * Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) +* Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index a4686e774b..d7070015ff 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -109,6 +109,7 @@ function initialize() { FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, + TimeUnit, RouteByAddress, Routes, SingleNodeRoute, @@ -203,6 +204,7 @@ function initialize() { SlotIdTypes, SlotKeyTypes, StreamEntries, + TimeUnit, ReturnTypeXinfoStream, RouteByAddress, Routes, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d445b60497..4d46a08c75 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -82,6 +82,7 @@ import { createGet, createGetBit, createGetDel, + createGetEx, createGetRange, createHDel, createHExists, @@ -204,6 +205,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + TimeUnit, } from "./Commands"; import { ClosingError, @@ -933,6 +935,32 @@ export class BaseClient { return this.createWritePromise(createGet(key), { decoder: decoder }); } + /** + * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. + * + * See https://valkey.io/commands/getex for more details. + * + * @param key - The key to retrieve from the database. + * @param options - (Optional) Set expiriation to the given key. + * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. + * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * @returns If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. + * + * since - Valkey 6.2.0 and above. + * + * @example + * ```typescript + * const result = await client.getex("key", {expiry: { type: TimeUnit.Seconds, count: 5 }}); + * console.log(result); // Output: 'value' + * ``` + */ + public async getex( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, + ): Promise { + return this.createWritePromise(createGetEx(key, options)); + } + /** * Gets a string value associated with the given `key`and deletes the key. * @@ -1005,7 +1033,7 @@ export class BaseClient { * console.log(result); // Output: 'OK' * * // Example usage of set method with conditional options and expiration - * const result2 = await client.set("key", "new_value", {conditionalSet: "onlyIfExists", expiry: { type: "seconds", count: 5 }}); + * const result2 = await client.set("key", "new_value", {conditionalSet: "onlyIfExists", expiry: { type: TimeUnit.Seconds, count: 5 }}); * console.log(result2); // Output: 'OK' - Set "new_value" to "key" only if "key" already exists, and set the key expiration to 5 seconds. * * // Example usage of set method with conditional options and returning old value diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 47094c43b3..d98d683748 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -142,26 +142,7 @@ export type SetOptions = { */ | "keepExisting" | { - type: /** - * Set the specified expire time, in seconds. Equivalent to - * `EX` in the Redis API. - */ - | "seconds" - /** - * Set the specified expire time, in milliseconds. Equivalent - * to `PX` in the Redis API. - */ - | "milliseconds" - /** - * Set the specified Unix time at which the key will expire, - * in seconds. Equivalent to `EXAT` in the Redis API. - */ - | "unixSeconds" - /** - * Set the specified Unix time at which the key will expire, - * in milliseconds. Equivalent to `PXAT` in the Redis API. - */ - | "unixMilliseconds"; + type: TimeUnit; count: number; }; }; @@ -187,28 +168,23 @@ export function createSet( args.push("GET"); } - if ( - options.expiry && - options.expiry !== "keepExisting" && - !Number.isInteger(options.expiry.count) - ) { - throw new Error( - `Received expiry '${JSON.stringify( - options.expiry, - )}'. Count must be an integer`, - ); - } + if (options.expiry) { + if ( + options.expiry !== "keepExisting" && + !Number.isInteger(options.expiry.count) + ) { + throw new Error( + `Received expiry '${JSON.stringify( + options.expiry, + )}'. Count must be an integer`, + ); + } - if (options.expiry === "keepExisting") { - args.push("KEEPTTL"); - } else if (options.expiry?.type === "seconds") { - args.push("EX", options.expiry.count.toString()); - } else if (options.expiry?.type === "milliseconds") { - args.push("PX", options.expiry.count.toString()); - } else if (options.expiry?.type === "unixSeconds") { - args.push("EXAT", options.expiry.count.toString()); - } else if (options.expiry?.type === "unixMilliseconds") { - args.push("PXAT", options.expiry.count.toString()); + if (options.expiry === "keepExisting") { + args.push("KEEPTTL"); + } else { + args.push(options.expiry.type, options.expiry.count.toString()); + } } } @@ -3640,3 +3616,57 @@ export function createBZPopMin( ): command_request.Command { return createCommand(RequestType.BZPopMin, [...keys, timeout.toString()]); } + +/** + * Time unit representation which is used in optional arguments for {@link BaseClient.getex|getex} and {@link BaseClient.set|set} command. + */ +export enum TimeUnit { + /** + * Set the specified expire time, in seconds. Equivalent to + * `EX` in the VALKEY API. + */ + Seconds = "EX", + /** + * Set the specified expire time, in milliseconds. Equivalent + * to `PX` in the VALKEY API. + */ + Milliseconds = "PX", + /** + * Set the specified Unix time at which the key will expire, + * in seconds. Equivalent to `EXAT` in the VALKEY API. + */ + UnixSeconds = "EXAT", + /** + * Set the specified Unix time at which the key will expire, + * in milliseconds. Equivalent to `PXAT` in the VALKEY API. + */ + UnixMilliseconds = "PXAT", +} + +/** + * @internal + */ +export function createGetEx( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, +): command_request.Command { + const args = [key]; + + if (options) { + if (options !== "persist" && !Number.isInteger(options.duration)) { + throw new Error( + `Received expiry '${JSON.stringify( + options.duration, + )}'. Count must be an integer`, + ); + } + + if (options === "persist") { + args.push("PERSIST"); + } else { + args.push(options.type, options.duration.toString()); + } + } + + return createCommand(RequestType.GetEx, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b1441ea566..3da471684c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -106,6 +106,7 @@ import { createGet, createGetBit, createGetDel, + createGetEx, createGetRange, createHDel, createHExists, @@ -240,6 +241,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + TimeUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -293,6 +295,7 @@ export class BaseTransaction> { } /** Get the value associated with the given key, or null if no such value exists. + * * See https://valkey.io/commands/get/ for details. * * @param key - The key to retrieve from the database. @@ -303,6 +306,26 @@ export class BaseTransaction> { return this.addAndReturn(createGet(key)); } + /** + * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. + * See https://valkey.io/commands/getex for more details. + * + * @param key - The key to retrieve from the database. + * @param options - (Optional) set expiriation to the given key. + * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. + * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * + * since - Valkey 6.2.0 and above. + * + * Command Response - If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. + */ + public getex( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, + ): T { + return this.addAndReturn(createGetEx(key, options)); + } + /** * Gets a string value associated with the given `key`and deletes the key. * diff --git a/node/tests/GlideClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts index de3af5e4d4..72ee0fe8f1 100644 --- a/node/tests/GlideClientInternals.test.ts +++ b/node/tests/GlideClientInternals.test.ts @@ -32,6 +32,7 @@ import { RequestError, ReturnType, SlotKeyTypes, + TimeUnit, } from ".."; import { command_request, @@ -545,7 +546,7 @@ describe("SocketConnectionInternals", () => { const request1 = connection.set("foo", "bar", { conditionalSet: "onlyIfExists", returnOldValue: true, - expiry: { type: "seconds", count: 10 }, + expiry: { type: TimeUnit.Seconds, count: 10 }, }); expect(await request1).toMatch("OK"); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5c24fd6f78..19228485c3 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -39,6 +39,7 @@ import { Script, SignedEncoding, SortOrder, + TimeUnit, Transaction, UnsignedEncoding, UpdateByScore, @@ -5672,7 +5673,7 @@ export function runBaseTests(config: { const value = uuidv4(); const setResWithExpirySetMilli = await client.set(key, value, { expiry: { - type: "milliseconds", + type: TimeUnit.Milliseconds, count: 500, }, }); @@ -5682,7 +5683,7 @@ export function runBaseTests(config: { const setResWithExpirySec = await client.set(key, value, { expiry: { - type: "seconds", + type: TimeUnit.Seconds, count: 1, }, }); @@ -5692,7 +5693,7 @@ export function runBaseTests(config: { const setWithUnixSec = await client.set(key, value, { expiry: { - type: "unixSeconds", + type: TimeUnit.UnixSeconds, count: Math.floor(Date.now() / 1000) + 1, }, }); @@ -5714,7 +5715,7 @@ export function runBaseTests(config: { expect(getResExpire).toEqual(null); const setResWithExpiryWithUmilli = await client.set(key, value, { expiry: { - type: "unixMilliseconds", + type: TimeUnit.UnixMilliseconds, count: Date.now() + 1000, }, }); @@ -5806,7 +5807,7 @@ export function runBaseTests(config: { // * returns the old value const setResWithAllOptions = await client.set(key, value, { expiry: { - type: "unixSeconds", + type: TimeUnit.UnixSeconds, count: Math.floor(Date.now() / 1000) + 1, }, conditionalSet: "onlyIfExists", @@ -5823,10 +5824,10 @@ export function runBaseTests(config: { const value = uuidv4(); const count = 2; const expiryCombination = [ - { type: "seconds", count }, - { type: "unixSeconds", count }, - { type: "unixMilliseconds", count }, - { type: "milliseconds", count }, + { type: TimeUnit.Seconds, count }, + { type: TimeUnit.Milliseconds, count }, + { type: TimeUnit.UnixSeconds, count }, + { type: TimeUnit.UnixMilliseconds, count }, "keepExisting", ]; let exist = false; @@ -5837,10 +5838,10 @@ export function runBaseTests(config: { | "keepExisting" | { type: - | "seconds" - | "milliseconds" - | "unixSeconds" - | "unixMilliseconds"; + | TimeUnit.Seconds + | TimeUnit.Milliseconds + | TimeUnit.UnixSeconds + | TimeUnit.UnixMilliseconds; count: number; }, conditionalSet: "onlyIfDoesNotExist", @@ -5863,10 +5864,10 @@ export function runBaseTests(config: { | "keepExisting" | { type: - | "seconds" - | "milliseconds" - | "unixSeconds" - | "unixMilliseconds"; + | TimeUnit.Seconds + | TimeUnit.Milliseconds + | TimeUnit.UnixSeconds + | TimeUnit.UnixMilliseconds; count: number; }, @@ -8584,6 +8585,51 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `getex test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const value = uuidv4(); + + expect(await client.set(key1, value)).toBe("OK"); + expect(await client.getex(key1)).toEqual(value); + expect(await client.ttl(key1)).toBe(-1); + + expect( + await client.getex(key1, { + type: TimeUnit.Seconds, + duration: 15, + }), + ).toEqual(value); + expect(await client.ttl(key1)).toBeGreaterThan(0); + expect(await client.getex(key1, "persist")).toEqual(value); + expect(await client.ttl(key1)).toBe(-1); + + // non existent key + expect(await client.getex(key2)).toBeNull(); + + // invalid time measurement + await expect( + client.getex(key1, { + type: TimeUnit.Seconds, + duration: -10, + }), + ).rejects.toThrow(RequestError); + + // Key exists, but is not a string + expect(await client.sadd(key2, ["a"])).toBe(1); + await expect(client.getex(key2)).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 25f9c77d24..a55ec380e6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -33,6 +33,7 @@ import { ScoreFilter, SignedEncoding, SortOrder, + TimeUnit, Transaction, UnsignedEncoding, } from ".."; @@ -659,8 +660,21 @@ export async function transactionTest( responseData.push(["flushdb(FlushMode.SYNC)", "OK"]); baseTransaction.dbsize(); responseData.push(["dbsize()", 0]); - baseTransaction.set(key1, "bar"); + baseTransaction.set(key1, "foo"); responseData.push(['set(key1, "bar")', "OK"]); + baseTransaction.set(key1, "bar", { returnOldValue: true }); + responseData.push(['set(key1, "bar", {returnOldValue: true})', "foo"]); + + if (gte(version, "6.2.0")) { + baseTransaction.getex(key1); + responseData.push(["getex(key1)", "bar"]); + baseTransaction.getex(key1, { type: TimeUnit.Seconds, duration: 1 }); + responseData.push([ + 'getex(key1, {expiry: { type: "seconds", count: 1 }})', + "bar", + ]); + } + baseTransaction.randomKey(); responseData.push(["randomKey()", key1]); baseTransaction.getrange(key1, 0, -1); From edcb15a4e42065c82fabd3f9f2fe81fc1404239c Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:51:37 -0700 Subject: [PATCH 171/236] Node: Add XRANGE command (#2069) * Update changelog Signed-off-by: Jonathan Louie * Implement XRANGE command Signed-off-by: Jonathan Louie * Update CHANGELOG.md Signed-off-by: Jonathan Louie * Apply prettier Signed-off-by: Jonathan Louie * Fix style issue Signed-off-by: Jonathan Louie * Fix failing test Signed-off-by: Jonathan Louie * Fix another failing test Signed-off-by: Jonathan Louie * Address PR comments Signed-off-by: Jonathan Louie * Change return type for xrange Signed-off-by: Jonathan Louie * Try to address remaining PR comments Signed-off-by: Jonathan Louie * Switch to ScoreBoundary Signed-off-by: Jonathan Louie * Remove merge conflict marker Signed-off-by: Jonathan Louie * Apply prettier for SharedTests Signed-off-by: Jonathan Louie * Replace remaining instances of StreamRangeBound with ScoreBoundary Signed-off-by: Jonathan Louie * Update test to use string instead of number Signed-off-by: Jonathan Louie * Change ScoreBoundary to generic Boundary and add check for Redis version to xrange tests Signed-off-by: Jonathan Louie * Adjust documentation and fix linting and tests Signed-off-by: Jonathan Louie * Adjust documentation Signed-off-by: Jonathan Louie --------- Signed-off-by: Jonathan Louie --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 8 +- node/src/BaseClient.ts | 74 +++++++--- node/src/Commands.ts | 87 +++++++----- node/src/Transaction.ts | 47 ++++-- node/tests/SharedTests.ts | 275 +++++++++++++++++++++++++----------- node/tests/TestUtilities.ts | 28 ++-- 7 files changed, 357 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54f490f7b..b14d1e50c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) +* Node: Added XRANGE command ([#2069](https://github.com/valkey-io/valkey-glide/pull/2069)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added WAIT command ([#2113](https://github.com/valkey-io/valkey-glide/pull/2113)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index d7070015ff..827d93ee45 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -124,8 +124,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - InfScoreBoundary, - ScoreBoundary, + InfBoundary, + Boundary, UpdateOptions, ProtocolVersion, RangeByIndex, @@ -220,8 +220,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - InfScoreBoundary, - ScoreBoundary, + InfBoundary, + Boundary, UpdateOptions, ProtocolVersion, RangeByIndex, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4d46a08c75..36bc60f95e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -22,6 +22,7 @@ import { BitOffsetOptions, BitmapIndexType, BitwiseOperation, + Boundary, CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, GeoAddOptions, @@ -41,7 +42,6 @@ import { RangeByLex, RangeByScore, ReturnTypeXinfoStream, - ScoreBoundary, ScoreFilter, SearchOrigin, SetOptions, @@ -176,6 +176,7 @@ import { createXInfoStream, createXLen, createXPending, + createXRange, createXRead, createXTrim, createZAdd, @@ -2980,6 +2981,45 @@ export class BaseClient { return this.createWritePromise(scriptInvocation); } + /** + * Returns stream entries matching a given range of entry IDs. + * + * See https://valkey.io/commands/xrange for more details. + * + * @param key - The key of the stream. + * @param start - The starting stream entry ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.NegativeInfinity` to start with the minimum available ID. + * @param end - The ending stream entry ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. + * @param count - An optional argument specifying the maximum count of stream entries to return. + * If `count` is not provided, all stream entries in the range will be returned. + * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is negative. + * + * @example + * ```typescript + * await client.xadd("mystream", [["field1", "value1"]], {id: "0-1"}); + * await client.xadd("mystream", [["field2", "value2"], ["field2", "value3"]], {id: "0-2"}); + * console.log(await client.xrange("mystream", InfBoundary.NegativeInfinity, InfBoundary.PositiveInfinity)); + * // Output: + * // { + * // "0-1": [["field1", "value1"]], + * // "0-2": [["field2", "value2"], ["field2", "value3"]], + * // } // Indicates the stream entry IDs and their associated field-value pairs for all stream entries in "mystream". + * ``` + */ + public async xrange( + key: string, + start: Boundary, + end: Boundary, + count?: number, + ): Promise | null> { + return this.createWritePromise(createXRange(key, start, end, count)); + } + /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. * See https://valkey.io/commands/zadd/ for more details. @@ -3277,7 +3317,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of the zcount method to count members in a sorted set within a score range - * const result = await client.zcount("my_sorted_set", { value: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); + * const result = await client.zcount("my_sorted_set", { value: 5.0, isInclusive: true }, InfBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that there are 2 members with scores between 5.0 (inclusive) and +inf in the sorted set "my_sorted_set". * ``` * @@ -3290,8 +3330,8 @@ export class BaseClient { */ public zcount( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): Promise { return this.createWritePromise(createZCount(key, minScore, maxScore)); } @@ -3321,7 +3361,7 @@ export class BaseClient { * ```typescript * // Example usage of zrange method to retrieve members within a score range in ascending order * const result = await client.zrange("my_sorted_set", { - * start: InfScoreBoundary.NegativeInfinity, + * start: InfBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -3363,7 +3403,7 @@ export class BaseClient { * ```typescript * // Example usage of zrangeWithScores method to retrieve members within a score range with their scores * const result = await client.zrangeWithScores("my_sorted_set", { - * start: InfScoreBoundary.NegativeInfinity, + * start: InfBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -3409,7 +3449,7 @@ export class BaseClient { * ```typescript * // Example usage of zrangeStore method to retrieve members within a score range in ascending order and store in "destination_key" * const result = await client.zrangeStore("destination_key", "my_sorted_set", { - * start: InfScoreBoundary.NegativeInfinity, + * start: InfBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -3805,14 +3845,14 @@ export class BaseClient { * @example * ```typescript * // Example usage of zremRangeByLex method when the sorted set does not exist - * const result = await client.zremRangeByLex("non_existing_sorted_set", InfScoreBoundary.NegativeInfinity, { value: "e" }); + * const result = await client.zremRangeByLex("non_existing_sorted_set", InfBoundary.NegativeInfinity, { value: "e" }); * console.log(result); // Output: 0 - Indicates that no elements were removed. * ``` */ public zremRangeByLex( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): Promise { return this.createWritePromise( createZRemRangeByLex(key, minLex, maxLex), @@ -3832,7 +3872,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of zremRangeByScore method to remove members from a sorted set based on score range - * const result = await client.zremRangeByScore("my_sorted_set", { value: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); + * const result = await client.zremRangeByScore("my_sorted_set", { value: 5.0, isInclusive: true }, InfBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that 2 members with scores between 5.0 (inclusive) and +inf have been removed from the sorted set "my_sorted_set". * ``` * @@ -3845,8 +3885,8 @@ export class BaseClient { */ public zremRangeByScore( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): Promise { return this.createWritePromise( createZRemRangeByScore(key, minScore, maxScore), @@ -3867,7 +3907,7 @@ export class BaseClient { * * @example * ```typescript - * const result = await client.zlexcount("my_sorted_set", {value: "c"}, InfScoreBoundary.PositiveInfinity); + * const result = await client.zlexcount("my_sorted_set", {value: "c"}, InfBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that there are 2 members with lex scores between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set". * ``` * @@ -3879,8 +3919,8 @@ export class BaseClient { */ public async zlexcount( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): Promise { return this.createWritePromise(createZLexCount(key, minLex, maxLex)); } @@ -4132,7 +4172,7 @@ export class BaseClient { * ```typescript * console.log(await client.xpending("my_stream", "my_group"), { * start: { value: "0-1", isInclusive: true }, - * end: InfScoreBoundary.PositiveInfinity, + * end: InfBoundary.PositiveInfinity, * count: 2, * consumer: "consumer1" * }); // Output: diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d98d683748..dee2022c9d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1518,35 +1518,35 @@ export function createZMScore( return createCommand(RequestType.ZMScore, [key, ...members]); } -export enum InfScoreBoundary { +export enum InfBoundary { /** - * Positive infinity bound for sorted set. + * Positive infinity bound. */ PositiveInfinity = "+", /** - * Negative infinity bound for sorted set. + * Negative infinity bound. */ NegativeInfinity = "-", } /** - * Defines where to insert new elements into a list. + * Defines the boundaries of a range. */ -export type ScoreBoundary = +export type Boundary = /** - * Represents an lower/upper boundary in a sorted set. + * Represents an lower/upper boundary. */ - | InfScoreBoundary + | InfBoundary /** - * Represents a specific numeric score boundary in a sorted set. + * Represents a specific boundary. */ | { /** - * The score value. + * The comparison value. */ value: T; /** - * Whether the score value is inclusive. Defaults to True. + * Whether the value is inclusive. Defaults to `true`. */ isInclusive?: boolean; }; @@ -1574,11 +1574,11 @@ type SortedSetRange = { /** * The start boundary. */ - start: ScoreBoundary; + start: Boundary; /** * The stop boundary. */ - stop: ScoreBoundary; + stop: Boundary; /** * The limit argument for a range query. * Represents a limit argument for a range query in a sorted set to @@ -1605,10 +1605,10 @@ export type RangeByLex = SortedSetRange & { type: "byLex" }; /** Returns a string representation of a score boundary as a command argument. */ function getScoreBoundaryArg( - score: ScoreBoundary | ScoreBoundary, + score: Boundary | Boundary, ): string { if (typeof score === "string") { - // InfScoreBoundary + // InfBoundary return score + "inf"; } @@ -1620,11 +1620,9 @@ function getScoreBoundaryArg( } /** Returns a string representation of a lex boundary as a command argument. */ -function getLexBoundaryArg( - score: ScoreBoundary | ScoreBoundary, -): string { +function getLexBoundaryArg(score: Boundary | Boundary): string { if (typeof score === "string") { - // InfScoreBoundary + // InfBoundary return score; } @@ -1637,18 +1635,18 @@ function getLexBoundaryArg( /** Returns a string representation of a stream boundary as a command argument. */ function getStreamBoundaryArg( - score: ScoreBoundary | ScoreBoundary, + boundary: Boundary | Boundary, ): string { - if (typeof score === "string") { - // InfScoreBoundary - return score; + if (typeof boundary === "string") { + // InfBoundary + return boundary; } - if (score.isInclusive == false) { - return "(" + score.value.toString(); + if (boundary.isInclusive == false) { + return "(" + boundary.value.toString(); } - return score.value.toString(); + return boundary.value.toString(); } function createZRangeArgs( @@ -1704,8 +1702,8 @@ function createZRangeArgs( */ export function createZCount( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): command_request.Command { const args = [ key, @@ -1861,8 +1859,8 @@ export function createZRemRangeByRank( */ export function createZRemRangeByLex( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): command_request.Command { const args = [key, getLexBoundaryArg(minLex), getLexBoundaryArg(maxLex)]; return createCommand(RequestType.ZRemRangeByLex, args); @@ -1873,8 +1871,8 @@ export function createZRemRangeByLex( */ export function createZRemRangeByScore( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): command_request.Command { const args = [ key, @@ -1894,8 +1892,8 @@ export function createPersist(key: string): command_request.Command { */ export function createZLexCount( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): command_request.Command { const args = [key, getLexBoundaryArg(minLex), getLexBoundaryArg(maxLex)]; return createCommand(RequestType.ZLexCount, args); @@ -2040,6 +2038,25 @@ export function createXTrim( return createCommand(RequestType.XTrim, args); } +/** + * @internal + */ +export function createXRange( + key: string, + start: Boundary, + end: Boundary, + count?: number, +): command_request.Command { + const args = [key, getStreamBoundaryArg(start), getStreamBoundaryArg(end)]; + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.XRange, args); +} + /** * @internal */ @@ -2424,9 +2441,9 @@ export type StreamPendingOptions = { /** Filter pending entries by their idle time - in milliseconds */ minIdleTime?: number; /** Starting stream ID bound for range. */ - start: ScoreBoundary; + start: Boundary; /** Ending stream ID bound for range. */ - end: ScoreBoundary; + end: Boundary; /** Limit the number of messages returned. */ count: number; /** Filter pending entries by consumer. */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3da471684c..2d90524dd9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -21,6 +21,7 @@ import { BitOffsetOptions, BitmapIndexType, BitwiseOperation, + Boundary, CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars ExpireOptions, FlushMode, @@ -46,7 +47,6 @@ import { RangeByLex, RangeByScore, ReturnTypeXinfoStream, // eslint-disable-line @typescript-eslint/no-unused-vars - ScoreBoundary, ScoreFilter, SearchOrigin, SetOptions, @@ -212,6 +212,7 @@ import { createXInfoStream, createXLen, createXPending, + createXRange, createXRead, createXTrim, createZAdd, @@ -1779,8 +1780,8 @@ export class BaseTransaction> { */ public zcount( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): T { return this.addAndReturn(createZCount(key, minScore, maxScore)); } @@ -2082,8 +2083,8 @@ export class BaseTransaction> { */ public zremRangeByLex( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): T { return this.addAndReturn(createZRemRangeByLex(key, minLex, maxLex)); } @@ -2101,8 +2102,8 @@ export class BaseTransaction> { */ public zremRangeByScore( key: string, - minScore: ScoreBoundary, - maxScore: ScoreBoundary, + minScore: Boundary, + maxScore: Boundary, ): T { return this.addAndReturn( createZRemRangeByScore(key, minScore, maxScore), @@ -2124,8 +2125,8 @@ export class BaseTransaction> { */ public zlexcount( key: string, - minLex: ScoreBoundary, - maxLex: ScoreBoundary, + minLex: Boundary, + maxLex: Boundary, ): T { return this.addAndReturn(createZLexCount(key, minLex, maxLex)); } @@ -2330,6 +2331,34 @@ export class BaseTransaction> { return this.addAndReturn(createTime()); } + /** + * Returns stream entries matching a given range of entry IDs. + * + * See https://valkey.io/commands/xrange for more details. + * + * @param key - The key of the stream. + * @param start - The starting stream entry ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.NegativeInfinity` to start with the minimum available ID. + * @param end - The ending stream ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. + * @param count - An optional argument specifying the maximum count of stream entries to return. + * If `count` is not provided, all stream entries in the range will be returned. + * + * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is negative. + */ + public xrange( + key: string, + start: Boundary, + end: Boundary, + count?: number, + ): T { + return this.addAndReturn(createXRange(key, start, end, count)); + } + /** * Reads entries from the given streams. * See https://valkey.io/commands/xread/ for more details. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 19228485c3..0936117791 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,7 +29,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, - InfScoreBoundary, + InfBoundary, InfoOptions, InsertPosition, ListDirection, @@ -3675,8 +3675,8 @@ export function runBaseTests(config: { expect( await client.zcount( key1, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(3); expect( @@ -3694,28 +3694,20 @@ export function runBaseTests(config: { ), ).toEqual(2); expect( - await client.zcount( - key1, - InfScoreBoundary.NegativeInfinity, - { - value: 3, - }, - ), + await client.zcount(key1, InfBoundary.NegativeInfinity, { + value: 3, + }), ).toEqual(3); expect( - await client.zcount( - key1, - InfScoreBoundary.PositiveInfinity, - { - value: 3, - }, - ), + await client.zcount(key1, InfBoundary.PositiveInfinity, { + value: 3, + }), ).toEqual(0); expect( await client.zcount( "nonExistingKey", - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(0); @@ -3723,8 +3715,8 @@ export function runBaseTests(config: { await expect( client.zcount( key2, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).rejects.toThrow(); }, protocol); @@ -3779,14 +3771,14 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), ).toEqual(["one", "two"]); const result = await client.zrangeWithScores(key, { - start: InfScoreBoundary.NegativeInfinity, - stop: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + stop: InfBoundary.PositiveInfinity, type: "byScore", }); @@ -3802,7 +3794,7 @@ export function runBaseTests(config: { key, { start: { value: 3, isInclusive: false }, - stop: InfScoreBoundary.NegativeInfinity, + stop: InfBoundary.NegativeInfinity, type: "byScore", }, true, @@ -3811,8 +3803,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.NegativeInfinity, - stop: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + stop: InfBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byScore", }), @@ -3822,7 +3814,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3832,7 +3824,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3842,7 +3834,7 @@ export function runBaseTests(config: { await client.zrangeWithScores( key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3852,7 +3844,7 @@ export function runBaseTests(config: { expect( await client.zrangeWithScores(key, { - start: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3872,7 +3864,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -3880,8 +3872,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.NegativeInfinity, - stop: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + stop: InfBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byLex", }), @@ -3892,7 +3884,7 @@ export function runBaseTests(config: { key, { start: { value: "c", isInclusive: false }, - stop: InfScoreBoundary.NegativeInfinity, + stop: InfBoundary.NegativeInfinity, type: "byLex", }, true, @@ -3903,7 +3895,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }, @@ -3913,7 +3905,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.PositiveInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -3989,7 +3981,7 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -4007,7 +3999,7 @@ export function runBaseTests(config: { key, { start: { value: 3, isInclusive: false }, - stop: InfScoreBoundary.NegativeInfinity, + stop: InfBoundary.NegativeInfinity, type: "byScore", }, true, @@ -4026,8 +4018,8 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.NegativeInfinity, - stop: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + stop: InfBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byScore", }), @@ -4044,7 +4036,7 @@ export function runBaseTests(config: { destkey, key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -4054,7 +4046,7 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -4076,7 +4068,7 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -4090,8 +4082,8 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.NegativeInfinity, - stop: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + stop: InfBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byLex", }), @@ -4109,7 +4101,7 @@ export function runBaseTests(config: { key, { start: { value: "c", isInclusive: false }, - stop: InfScoreBoundary.NegativeInfinity, + stop: InfBoundary.NegativeInfinity, type: "byLex", }, true, @@ -4131,7 +4123,7 @@ export function runBaseTests(config: { destkey, key, { - start: InfScoreBoundary.NegativeInfinity, + start: InfBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }, @@ -4141,7 +4133,7 @@ export function runBaseTests(config: { expect( await client.zrangeStore(destkey, key, { - start: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.PositiveInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -4975,6 +4967,131 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xrange test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = uuidv4(); + const nonExistingKey = uuidv4(); + const stringKey = uuidv4(); + const streamId1 = "0-1"; + const streamId2 = "0-2"; + const streamId3 = "0-3"; + + expect( + await client.xadd(key, [["f1", "v1"]], { id: streamId1 }), + ).toEqual(streamId1); + expect( + await client.xadd(key, [["f2", "v2"]], { id: streamId2 }), + ).toEqual(streamId2); + expect(await client.xlen(key)).toEqual(2); + + // get everything from the stream + expect( + await client.xrange( + key, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + ), + ).toEqual({ + [streamId1]: [["f1", "v1"]], + [streamId2]: [["f2", "v2"]], + }); + + // returns empty mapping if + before - + expect( + await client.xrange( + key, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + ), + ).toEqual({}); + + expect( + await client.xadd(key, [["f3", "v3"]], { id: streamId3 }), + ).toEqual(streamId3); + + // get the newest entry + if (!cluster.checkIfServerVersionLessThan("6.2.0")) { + expect( + await client.xrange( + key, + { isInclusive: false, value: streamId2 }, + { value: "5" }, + 1, + ), + ).toEqual({ [streamId3]: [["f3", "v3"]] }); + } + + // xrange against an emptied stream + expect( + await client.xdel(key, [streamId1, streamId2, streamId3]), + ).toEqual(3); + expect( + await client.xrange( + key, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + 10, + ), + ).toEqual({}); + + expect( + await client.xrange( + nonExistingKey, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + ), + ).toEqual({}); + + // count value < 1 returns null + expect( + await client.xrange( + key, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + 0, + ), + ).toEqual(null); + expect( + await client.xrange( + key, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + -1, + ), + ).toEqual(null); + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")); + await expect( + client.xrange( + stringKey, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + ), + ).rejects.toThrow(RequestError); + + // invalid start bound + await expect( + client.xrange( + key, + { value: "not_a_stream_id" }, + InfBoundary.PositiveInfinity, + ), + ).rejects.toThrow(RequestError); + + // invalid end bound + await expect( + client.xrange(key, InfBoundary.NegativeInfinity, { + value: "not_a_stream_id", + }), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zremRangeByLex test_%p`, async (protocol) => { @@ -4996,7 +5113,7 @@ export function runBaseTests(config: { await client.zremRangeByLex( key, { value: "d" }, - InfScoreBoundary.PositiveInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(1); @@ -5005,15 +5122,15 @@ export function runBaseTests(config: { await client.zremRangeByLex( key, { value: "a" }, - InfScoreBoundary.NegativeInfinity, + InfBoundary.NegativeInfinity, ), ).toEqual(0); expect( await client.zremRangeByLex( "nonExistingKey", - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(0); @@ -5022,8 +5139,8 @@ export function runBaseTests(config: { await expect( client.zremRangeByLex( stringKey, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).rejects.toThrow(RequestError); }, protocol); @@ -5051,15 +5168,15 @@ export function runBaseTests(config: { await client.zremRangeByScore( key, { value: 1 }, - InfScoreBoundary.NegativeInfinity, + InfBoundary.NegativeInfinity, ), ).toEqual(0); expect( await client.zremRangeByScore( "nonExistingKey", - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(0); }, protocol); @@ -5080,8 +5197,8 @@ export function runBaseTests(config: { expect( await client.zlexcount( key, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(3); @@ -5090,40 +5207,32 @@ export function runBaseTests(config: { await client.zlexcount( key, { value: "a", isInclusive: false }, - InfScoreBoundary.PositiveInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(2); // In range negative infinity to c (inclusive) expect( - await client.zlexcount( - key, - InfScoreBoundary.NegativeInfinity, - { - value: "c", - isInclusive: true, - }, - ), + await client.zlexcount(key, InfBoundary.NegativeInfinity, { + value: "c", + isInclusive: true, + }), ).toEqual(3); // Incorrect range start > end expect( - await client.zlexcount( - key, - InfScoreBoundary.PositiveInfinity, - { - value: "c", - isInclusive: true, - }, - ), + await client.zlexcount(key, InfBoundary.PositiveInfinity, { + value: "c", + isInclusive: true, + }), ).toEqual(0); // Non-existing key expect( await client.zlexcount( "non_existing_key", - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).toEqual(0); @@ -5132,8 +5241,8 @@ export function runBaseTests(config: { await expect( client.zlexcount( stringKey, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ), ).rejects.toThrow(RequestError); }, protocol); @@ -8003,8 +8112,8 @@ export function runBaseTests(config: { ]); const result = await client.xpendingWithOptions(key, group, { - start: InfScoreBoundary.NegativeInfinity, - end: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + end: InfBoundary.PositiveInfinity, count: 1, minIdleTime: 42, }); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a55ec380e6..d90283399c 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -24,7 +24,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, - InfScoreBoundary, + InfBoundary, InsertPosition, ListDirection, ProtocolVersion, @@ -987,22 +987,18 @@ export async function transactionTest( responseData.push(["zinterstore(key12, [key12, key13])", 2]); } - baseTransaction.zcount( - key8, - { value: 2 }, - InfScoreBoundary.PositiveInfinity, - ); + baseTransaction.zcount(key8, { value: 2 }, InfBoundary.PositiveInfinity); responseData.push([ - "zcount(key8, { value: 2 }, InfScoreBoundary.PositiveInfinity)", + "zcount(key8, { value: 2 }, InfBoundary.PositiveInfinity)", 4, ]); baseTransaction.zlexcount( key8, { value: "a" }, - InfScoreBoundary.PositiveInfinity, + InfBoundary.PositiveInfinity, ); responseData.push([ - 'zlexcount(key8, { value: "a" }, InfScoreBoundary.PositiveInfinity)', + 'zlexcount(key8, { value: "a" }, InfBoundary.PositiveInfinity)', 4, ]); baseTransaction.zpopmin(key8); @@ -1021,14 +1017,14 @@ export async function transactionTest( responseData.push(["zremRangeByRank(key8, 1, 1)", 1]); baseTransaction.zremRangeByScore( key8, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ); responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty baseTransaction.zremRangeByLex( key8, - InfScoreBoundary.NegativeInfinity, - InfScoreBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, ); responseData.push(["zremRangeByLex(key8, -Inf, +Inf)", 0]); // key8 is already empty @@ -1074,6 +1070,8 @@ export async function transactionTest( ]); baseTransaction.xlen(key9); responseData.push(["xlen(key9)", 3]); + baseTransaction.xrange(key9, { value: "0-1" }, { value: "0-1" }); + responseData.push(["xrange(key9)", { "0-1": [["field", "value1"]] }]); baseTransaction.xread({ [key9]: "0-1" }); responseData.push([ 'xread({ [key9]: "0-1" })', @@ -1131,8 +1129,8 @@ export async function transactionTest( [1, "0-2", "0-2", [[consumer, "1"]]], ]); baseTransaction.xpendingWithOptions(key9, groupName1, { - start: InfScoreBoundary.NegativeInfinity, - end: InfScoreBoundary.PositiveInfinity, + start: InfBoundary.NegativeInfinity, + end: InfBoundary.PositiveInfinity, count: 10, }); responseData.push([ From 136c5b526fec116f90287a8547288aab5af57dc3 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 14 Aug 2024 15:02:35 -0700 Subject: [PATCH 172/236] Node: Add async to commands (#2110) * Node: Add async to commands Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 292 +++++++++++++++----------- node/src/GlideClient.ts | 46 ++-- node/src/GlideClusterClient.ts | 49 +++-- node/tests/GlideClusterClient.test.ts | 4 +- 5 files changed, 229 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b14d1e50c3..d5aa121333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ #### Changes * Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) +* Node: Update all commands to use `async` ([#2110](https://github.com/valkey-io/valkey-glide/pull/2110)) * Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) * Node: Added XPENDING commands ([#2085](https://github.com/valkey-io/valkey-glide/pull/2085)) * Node: Added HSCAN command ([#2098](https://github.com/valkey-io/valkey-glide/pull/2098/)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 36bc60f95e..b397f2f357 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -771,7 +771,7 @@ export class BaseClient { return [null, null]; } - public getPubSubMessage(): Promise { + public async getPubSubMessage(): Promise { if (this.isClosed) { throw new ClosingError( "Unable to execute requests; the client is closed. Please create a new client.", @@ -932,7 +932,7 @@ export class BaseClient { * console.log(result); // Output: {"data": [118, 97, 108, 117, 101], "type": "Buffer"} * ``` */ - public get(key: string, decoder?: Decoder): Promise { + public async get(key: string, decoder?: Decoder): Promise { return this.createWritePromise(createGet(key), { decoder: decoder }); } @@ -978,7 +978,7 @@ export class BaseClient { * const value = client.getdel("key"); // value is null * ``` */ - public getdel(key: string): Promise { + public async getdel(key: string): Promise { return this.createWritePromise(createGetDel(key)); } @@ -1046,7 +1046,7 @@ export class BaseClient { * console.log(result4); // Output: 'new_value' - Value wasn't modified back to being "value" because of "NX" flag. * ``` */ - public set( + public async set( key: string | Uint8Array, value: string | Uint8Array, options?: SetOptions, @@ -1075,7 +1075,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public del(keys: string[]): Promise { + public async del(keys: string[]): Promise { return this.createWritePromise(createDel(keys)); } @@ -1096,7 +1096,7 @@ export class BaseClient { * console.log(result); // Output: ['value1', 'value2'] * ``` */ - public mget(keys: string[]): Promise<(string | null)[]> { + public async mget(keys: string[]): Promise<(string | null)[]> { return this.createWritePromise(createMGet(keys)); } @@ -1114,7 +1114,7 @@ export class BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public mset(keyValueMap: Record): Promise<"OK"> { + public async mset(keyValueMap: Record): Promise<"OK"> { return this.createWritePromise(createMSet(keyValueMap)); } @@ -1155,7 +1155,7 @@ export class BaseClient { * console.log(result); // Output: 11 * ``` */ - public incr(key: string): Promise { + public async incr(key: string): Promise { return this.createWritePromise(createIncr(key)); } @@ -1174,7 +1174,7 @@ export class BaseClient { * console.log(result); // Output: 15 * ``` */ - public incrBy(key: string, amount: number): Promise { + public async incrBy(key: string, amount: number): Promise { return this.createWritePromise(createIncrBy(key, amount)); } @@ -1195,7 +1195,7 @@ export class BaseClient { * console.log(result); // Output: 13.0 * ``` */ - public incrByFloat(key: string, amount: number): Promise { + public async incrByFloat(key: string, amount: number): Promise { return this.createWritePromise(createIncrByFloat(key, amount)); } @@ -1213,7 +1213,7 @@ export class BaseClient { * console.log(result); // Output: 9 * ``` */ - public decr(key: string): Promise { + public async decr(key: string): Promise { return this.createWritePromise(createDecr(key)); } @@ -1232,7 +1232,7 @@ export class BaseClient { * console.log(result); // Output: 5 * ``` */ - public decrBy(key: string, amount: number): Promise { + public async decrBy(key: string, amount: number): Promise { return this.createWritePromise(createDecrBy(key, amount)); } @@ -1259,7 +1259,7 @@ export class BaseClient { * console.log(result2); // Output: "@" - "@" has binary value 01000000 * ``` */ - public bitop( + public async bitop( operation: BitwiseOperation, destination: string, keys: string[], @@ -1286,7 +1286,7 @@ export class BaseClient { * console.log(result); // Output: 1 - The second bit of the string stored at "key" is set to 1. * ``` */ - public getbit(key: string, offset: number): Promise { + public async getbit(key: string, offset: number): Promise { return this.createWritePromise(createGetBit(key, offset)); } @@ -1309,7 +1309,11 @@ export class BaseClient { * console.log(result); // Output: 0 - The second bit value was 0 before setting to 1. * ``` */ - public setbit(key: string, offset: number, value: number): Promise { + public async setbit( + key: string, + offset: number, + value: number, + ): Promise { return this.createWritePromise(createSetBit(key, offset, value)); } @@ -1475,7 +1479,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public hget(key: string, field: string): Promise { + public async hget(key: string, field: string): Promise { return this.createWritePromise(createHGet(key, field)); } @@ -1494,7 +1498,7 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that 2 fields were successfully set in the hash "my_hash". * ``` */ - public hset( + public async hset( key: string, fieldValueMap: Record, ): Promise { @@ -1525,7 +1529,11 @@ export class BaseClient { * console.log(result); // Output: false - Indicates that the field "field" already existed in the hash "my_hash" and was not set again. * ``` */ - public hsetnx(key: string, field: string, value: string): Promise { + public async hsetnx( + key: string, + field: string, + value: string, + ): Promise { return this.createWritePromise(createHSetNX(key, field, value)); } @@ -1545,7 +1553,7 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that two fields were successfully removed from the hash. * ``` */ - public hdel(key: string, fields: string[]): Promise { + public async hdel(key: string, fields: string[]): Promise { return this.createWritePromise(createHDel(key, fields)); } @@ -1565,7 +1573,10 @@ export class BaseClient { * console.log(result); // Output: ["value1", "value2"] - A list of values associated with the specified fields. * ``` */ - public hmget(key: string, fields: string[]): Promise<(string | null)[]> { + public async hmget( + key: string, + fields: string[], + ): Promise<(string | null)[]> { return this.createWritePromise(createHMGet(key, fields)); } @@ -1590,7 +1601,7 @@ export class BaseClient { * console.log(result); // Output: false * ``` */ - public hexists(key: string, field: string): Promise { + public async hexists(key: string, field: string): Promise { return this.createWritePromise(createHExists(key, field)); } @@ -1608,7 +1619,7 @@ export class BaseClient { * console.log(result); // Output: {"field1": "value1", "field2": "value2"} * ``` */ - public hgetall(key: string): Promise> { + public async hgetall(key: string): Promise> { return this.createWritePromise(createHGetAll(key)); } @@ -1629,7 +1640,7 @@ export class BaseClient { * console.log(result); // Output: 5 * ``` */ - public hincrBy( + public async hincrBy( key: string, field: string, amount: number, @@ -1654,7 +1665,7 @@ export class BaseClient { * console.log(result); // Output: '2.5' * ``` */ - public hincrByFloat( + public async hincrByFloat( key: string, field: string, amount: number, @@ -1682,7 +1693,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public hlen(key: string): Promise { + public async hlen(key: string): Promise { return this.createWritePromise(createHLen(key)); } @@ -1699,7 +1710,7 @@ export class BaseClient { * console.log(result); // Output: ["value1", "value2", "value3"] - Returns all the values stored in the hash "my_hash". * ``` */ - public hvals(key: string): Promise { + public async hvals(key: string): Promise { return this.createWritePromise(createHVals(key)); } @@ -1719,7 +1730,7 @@ export class BaseClient { * console.log(result); // Output: 5 * ``` */ - public hstrlen(key: string, field: string): Promise { + public async hstrlen(key: string, field: string): Promise { return this.createWritePromise(createHStrlen(key, field)); } @@ -1866,7 +1877,7 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that a new list was created with one element * ``` */ - public lpush(key: string, elements: string[]): Promise { + public async lpush(key: string, elements: string[]): Promise { return this.createWritePromise(createLPush(key, elements)); } @@ -1885,7 +1896,7 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that the list has two elements. * ``` */ - public lpushx(key: string, elements: string[]): Promise { + public async lpushx(key: string, elements: string[]): Promise { return this.createWritePromise(createLPushX(key, elements)); } @@ -1911,7 +1922,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public lpop(key: string): Promise { + public async lpop(key: string): Promise { return this.createWritePromise(createLPop(key)); } @@ -1937,7 +1948,10 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public lpopCount(key: string, count: number): Promise { + public async lpopCount( + key: string, + count: number, + ): Promise { return this.createWritePromise(createLPop(key, count)); } @@ -1976,7 +1990,11 @@ export class BaseClient { * console.log(result); // Output: [] * ``` */ - public lrange(key: string, start: number, end: number): Promise { + public async lrange( + key: string, + start: number, + end: number, + ): Promise { return this.createWritePromise(createLRange(key, start, end)); } @@ -1994,7 +2012,7 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that there are 3 elements in the list. * ``` */ - public llen(key: string): Promise { + public async llen(key: string): Promise { return this.createWritePromise(createLLen(key)); } @@ -2106,7 +2124,11 @@ export class BaseClient { * 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"> { + public async lset( + key: string, + index: number, + element: string, + ): Promise<"OK"> { return this.createWritePromise(createLSet(key, index, element)); } @@ -2131,7 +2153,7 @@ export class BaseClient { * console.log(result); // Output: 'OK' - Indicates that the list has been trimmed to contain elements from 0 to 1. * ``` */ - public ltrim(key: string, start: number, end: number): Promise<"OK"> { + public async ltrim(key: string, start: number, end: number): Promise<"OK"> { return this.createWritePromise(createLTrim(key, start, end)); } @@ -2153,7 +2175,11 @@ export class BaseClient { * console.log(result); // Output: 2 - Removes the first 2 occurrences of "value" in the list. * ``` */ - public lrem(key: string, count: number, element: string): Promise { + public async lrem( + key: string, + count: number, + element: string, + ): Promise { return this.createWritePromise(createLRem(key, count, element)); } @@ -2180,7 +2206,7 @@ export class BaseClient { * console.log(result); // Output: 1 * ``` */ - public rpush(key: string, elements: string[]): Promise { + public async rpush(key: string, elements: string[]): Promise { return this.createWritePromise(createRPush(key, elements)); } @@ -2199,7 +2225,7 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that the list has two elements. * ``` * */ - public rpushx(key: string, elements: string[]): Promise { + public async rpushx(key: string, elements: string[]): Promise { return this.createWritePromise(createRPushX(key, elements)); } @@ -2225,7 +2251,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public rpop(key: string): Promise { + public async rpop(key: string): Promise { return this.createWritePromise(createRPop(key)); } @@ -2251,7 +2277,10 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public rpopCount(key: string, count: number): Promise { + public async rpopCount( + key: string, + count: number, + ): Promise { return this.createWritePromise(createRPop(key, count)); } @@ -2270,7 +2299,7 @@ export class BaseClient { * console.log(result); // Output: 2 * ``` */ - public sadd(key: string, members: string[]): Promise { + public async sadd(key: string, members: string[]): Promise { return this.createWritePromise(createSAdd(key, members)); } @@ -2289,7 +2318,7 @@ export class BaseClient { * console.log(result); // Output: 2 * ``` */ - public srem(key: string, members: string[]): Promise { + public async srem(key: string, members: string[]): Promise { return this.createWritePromise(createSRem(key, members)); } @@ -2307,7 +2336,7 @@ export class BaseClient { * console.log(result); // Output: Set {'member1', 'member2', 'member3'} * ``` */ - public smembers(key: string): Promise> { + public async smembers(key: string): Promise> { return this.createWritePromise(createSMembers(key)).then( (smembes) => new Set(smembes), ); @@ -2330,7 +2359,7 @@ export class BaseClient { * console.log(result); // Output: true - "member1" was moved from "set1" to "set2". * ``` */ - public smove( + public async smove( source: string, destination: string, member: string, @@ -2353,7 +2382,7 @@ export class BaseClient { * console.log(result); // Output: 3 * ``` */ - public scard(key: string): Promise { + public async scard(key: string): Promise { return this.createWritePromise(createSCard(key)); } @@ -2379,7 +2408,7 @@ export class BaseClient { * console.log(result); // Output: Set {} - An empty set is returned since the key does not exist. * ``` */ - public sinter(keys: string[]): Promise> { + public async sinter(keys: string[]): Promise> { return this.createWritePromise(createSInter(keys)).then( (sinter) => new Set(sinter), ); @@ -2408,7 +2437,7 @@ export class BaseClient { * 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 { + public async sintercard(keys: string[], limit?: number): Promise { return this.createWritePromise(createSInterCard(keys, limit)); } @@ -2428,7 +2457,10 @@ export class BaseClient { * console.log(result); // Output: 2 - Two elements were stored at "my_set", and those elements are the intersection of "set1" and "set2". * ``` */ - public sinterstore(destination: string, keys: string[]): Promise { + public async sinterstore( + destination: string, + keys: string[], + ): Promise { return this.createWritePromise(createSInterStore(destination, keys)); } @@ -2450,7 +2482,7 @@ export class BaseClient { * console.log(result); // Output: Set {"member1"} - "member2" is in "set1" but not "set2" * ``` */ - public sdiff(keys: string[]): Promise> { + public async sdiff(keys: string[]): Promise> { return this.createWritePromise(createSDiff(keys)).then( (sdiff) => new Set(sdiff), ); @@ -2474,7 +2506,10 @@ export class BaseClient { * console.log(result); // Output: 1 - One member was stored in "set3", and that member is the diff between "set1" and "set2". * ``` */ - public sdiffstore(destination: string, keys: string[]): Promise { + public async sdiffstore( + destination: string, + keys: string[], + ): Promise { return this.createWritePromise(createSDiffStore(destination, keys)); } @@ -2499,7 +2534,7 @@ export class BaseClient { * console.log(result2); // Output: Set {'member1', 'member2'} * ``` */ - public sunion(keys: string[]): Promise> { + public async sunion(keys: string[]): Promise> { return this.createWritePromise(createSUnion(keys)).then( (sunion) => new Set(sunion), ); @@ -2522,7 +2557,10 @@ export class BaseClient { * console.log(length); // Output: 2 - Two elements were stored in "mySet", and those two members are the union of "set1" and "set2". * ``` */ - public sunionstore(destination: string, keys: string[]): Promise { + public async sunionstore( + destination: string, + keys: string[], + ): Promise { return this.createWritePromise(createSUnionStore(destination, keys)); } @@ -2548,7 +2586,7 @@ export class BaseClient { * console.log(result); // Output: false - Indicates that "non_existing_member" does not exist in the set "my_set". * ``` */ - public sismember(key: string, member: string): Promise { + public async sismember(key: string, member: string): Promise { return this.createWritePromise(createSIsMember(key, member)); } @@ -2570,7 +2608,10 @@ export class BaseClient { * 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 { + public async smismember( + key: string, + members: string[], + ): Promise { return this.createWritePromise(createSMIsMember(key, members)); } @@ -2596,7 +2637,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public spop(key: string): Promise { + public async spop(key: string): Promise { return this.createWritePromise(createSPop(key)); } @@ -2650,7 +2691,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public srandmember(key: string): Promise { + public async srandmember(key: string): Promise { return this.createWritePromise(createSRandMember(key)); } @@ -2700,7 +2741,7 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that all three keys exist in the database. * ``` */ - public exists(keys: string[]): Promise { + public async exists(keys: string[]): Promise { return this.createWritePromise(createExists(keys)); } @@ -2719,7 +2760,7 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that all three keys were unlinked from the database. * ``` */ - public unlink(keys: string[]): Promise { + public async unlink(keys: string[]): Promise { return this.createWritePromise(createUnlink(keys)); } @@ -2749,7 +2790,7 @@ export class BaseClient { * console.log(result); // Output: false - Indicates that "my_key" has an existing expiry. * ``` */ - public expire( + public async expire( key: string, seconds: number, option?: ExpireOptions, @@ -2776,7 +2817,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates that the expiration time for "my_key" was successfully set. * ``` */ - public expireAt( + public async expireAt( key: string, unixSeconds: number, option?: ExpireOptions, @@ -2834,7 +2875,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates that a timeout of 60,000 milliseconds has been set for "my_key". * ``` */ - public pexpire( + public async pexpire( key: string, milliseconds: number, option?: ExpireOptions, @@ -2863,7 +2904,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates that the expiration time for "my_key" was successfully set. * ``` */ - public pexpireAt( + public async pexpireAt( key: string, unixMilliseconds: number, option?: ExpireOptions, @@ -2928,7 +2969,7 @@ export class BaseClient { * console.log(result); // Output: -2 - Indicates that the key doesn't exist. * ``` */ - public ttl(key: string): Promise { + public async ttl(key: string): Promise { return this.createWritePromise(createTTL(key)); } @@ -2953,7 +2994,7 @@ export class BaseClient { * console.log(result); // Output: ['foo', 'bar'] * ``` */ - public invokeScript( + public async invokeScript( script: Script, option?: ScriptOptions, ): Promise { @@ -3045,7 +3086,7 @@ export class BaseClient { * console.log(result); // Output: 2 - Updates the scores of two existing members in the sorted set "existing_sorted_set." * ``` */ - public zadd( + public async zadd( key: string, membersScoresMap: Record, options?: ZAddOptions, @@ -3081,7 +3122,7 @@ export class BaseClient { * console.log(result); // Output: null - Indicates that the member in the sorted set haven't been updated. * ``` */ - public zaddIncr( + public async zaddIncr( key: string, member: string, increment: number, @@ -3115,7 +3156,7 @@ export class BaseClient { * console.log(result); // Output: 0 - Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. * ``` */ - public zrem(key: string, members: string[]): Promise { + public async zrem(key: string, members: string[]): Promise { return this.createWritePromise(createZRem(key, members)); } @@ -3140,7 +3181,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public zcard(key: string): Promise { + public async zcard(key: string): Promise { return this.createWritePromise(createZCard(key)); } @@ -3163,7 +3204,7 @@ export class BaseClient { * console.log(cardinality); // Output: 3 - The intersection of the sorted sets at "key1" and "key2" has a cardinality of 3. * ``` */ - public zintercard(keys: string[], limit?: number): Promise { + public async zintercard(keys: string[], limit?: number): Promise { return this.createWritePromise(createZInterCard(keys, limit)); } @@ -3189,7 +3230,7 @@ export class BaseClient { * console.log(result); // Output: ["member1"] - "member1" is in "zset1" but not "zset2" or "zset3". * ``` */ - public zdiff(keys: string[]): Promise { + public async zdiff(keys: string[]): Promise { return this.createWritePromise(createZDiff(keys)); } @@ -3215,7 +3256,9 @@ export class BaseClient { * console.log(result); // Output: {"member1": 1.0} - "member1" is in "zset1" but not "zset2" or "zset3". * ``` */ - public zdiffWithScores(keys: string[]): Promise> { + public async zdiffWithScores( + keys: string[], + ): Promise> { return this.createWritePromise(createZDiffWithScores(keys)); } @@ -3244,7 +3287,10 @@ export class BaseClient { * console.log(result2); // Output: ["member2"] - "member2" is now stored in "my_sorted_set". * ``` */ - public zdiffstore(destination: string, keys: string[]): Promise { + public async zdiffstore( + destination: string, + keys: string[], + ): Promise { return this.createWritePromise(createZDiffStore(destination, keys)); } @@ -3278,7 +3324,7 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public zscore(key: string, member: string): Promise { + public async zscore(key: string, member: string): Promise { return this.createWritePromise(createZScore(key, member)); } @@ -3300,7 +3346,10 @@ export class BaseClient { * console.log(result); // Output: [1.0, null, 2.0] - "member1" has a score of 1.0, "non_existent_member" does not exist in the sorted set, and "member2" has a score of 2.0. * ``` */ - public zmscore(key: string, members: string[]): Promise<(number | null)[]> { + public async zmscore( + key: string, + members: string[], + ): Promise<(number | null)[]> { return this.createWritePromise(createZMScore(key, members)); } @@ -3328,7 +3377,7 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that there is one member with score between 5.0 (inclusive) and 10.0 (exclusive) in the sorted set "my_sorted_set". * ``` */ - public zcount( + public async zcount( key: string, minScore: Boundary, maxScore: Boundary, @@ -3368,7 +3417,7 @@ export class BaseClient { * console.log(result); // Output: ['member2', 'member3'] - Returns members with scores within the range of negative infinity to 3, in ascending order. * ``` */ - public zrange( + public async zrange( key: string, rangeQuery: RangeByScore | RangeByLex | RangeByIndex, reverse: boolean = false, @@ -3410,7 +3459,7 @@ export class BaseClient { * console.log(result); // Output: {'member4': -2.0, 'member7': 1.5} - Returns members with scores within the range of negative infinity to 3, with their scores. * ``` */ - public zrangeWithScores( + public async zrangeWithScores( key: string, rangeQuery: RangeByScore | RangeByLex | RangeByIndex, reverse: boolean = false, @@ -3456,7 +3505,7 @@ export class BaseClient { * console.log(result); // Output: 5 - Stores 5 members with scores within the range of negative infinity to 3, in ascending order, in "destination_key". * ``` */ - public zrangeStore( + public async zrangeStore( destination: string, source: string, rangeQuery: RangeByScore | RangeByLex | RangeByIndex, @@ -3494,7 +3543,7 @@ export class BaseClient { * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. * ``` */ - public zinterstore( + public async zinterstore( destination: string, keys: string[] | KeyWeight[], aggregationType?: AggregationType, @@ -3614,7 +3663,7 @@ export class BaseClient { * console.log(len2); // Output: 0 * ``` */ - public strlen(key: string): Promise { + public async strlen(key: string): Promise { return this.createWritePromise(createStrlen(key)); } @@ -3640,7 +3689,7 @@ export class BaseClient { * console.log(type); // Output: 'list' * ``` */ - public type(key: string): Promise { + public async type(key: string): Promise { return this.createWritePromise(createType(key)); } @@ -3669,7 +3718,7 @@ export class BaseClient { * console.log(result); // Output: {'member3': 7.5 , 'member2': 8.0} - Indicates that 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set. * ``` */ - public zpopmin( + public async zpopmin( key: string, count?: number, ): Promise> { @@ -3729,7 +3778,7 @@ export class BaseClient { * console.log(result); // Output: {'member2': 8.0, 'member3': 7.5} - Indicates that 'member2' with a score of 8.0 and 'member3' with a score of 7.5 have been removed from the sorted set. * ``` */ - public zpopmax( + public async zpopmax( key: string, count?: number, ): Promise> { @@ -3791,7 +3840,7 @@ export class BaseClient { * console.log(result); // Output: -1 - Indicates that the key "key" has no associated expire. * ``` */ - public pttl(key: string): Promise { + public async pttl(key: string): Promise { return this.createWritePromise(createPTTL(key)); } @@ -3815,7 +3864,7 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that three elements have been removed from the sorted set "my_sorted_set" between ranks 0 and 2. * ``` */ - public zremRangeByRank( + public async zremRangeByRank( key: string, start: number, end: number, @@ -3849,7 +3898,7 @@ export class BaseClient { * console.log(result); // Output: 0 - Indicates that no elements were removed. * ``` */ - public zremRangeByLex( + public async zremRangeByLex( key: string, minLex: Boundary, maxLex: Boundary, @@ -3883,7 +3932,7 @@ export class BaseClient { * console.log(result); // Output: 0 - Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. * ``` */ - public zremRangeByScore( + public async zremRangeByScore( key: string, minScore: Boundary, maxScore: Boundary, @@ -3948,7 +3997,7 @@ export class BaseClient { * console.log(result); // Output: null - Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". * ``` */ - public zrank(key: string, member: string): Promise { + public async zrank(key: string, member: string): Promise { return this.createWritePromise(createZRank(key, member)); } @@ -3976,7 +4025,7 @@ export class BaseClient { * console.log(result); // Output: null - Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". * ``` */ - public zrankWithScore( + public async zrankWithScore( key: string, member: string, ): Promise { @@ -4001,7 +4050,7 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that "member2" has the second-highest score in the sorted set "my_sorted_set". * ``` */ - public zrevrank(key: string, member: string): Promise { + public async zrevrank(key: string, member: string): Promise { return this.createWritePromise(createZRevRank(key, member)); } @@ -4025,7 +4074,7 @@ export class BaseClient { * console.log(result); // Output: [1, 6.0] - Indicates that "member2" with score 6.0 has the second-highest score in the sorted set "my_sorted_set". * ``` */ - public zrevrankWithScore( + public async zrevrankWithScore( key: string, member: string, ): Promise<(number[] | null)[]> { @@ -4041,7 +4090,7 @@ export class BaseClient { * @param options - options detailing how to add to the stream. * @returns The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. */ - public xadd( + public async xadd( key: string, values: [string, string][], options?: StreamAddOptions, @@ -4065,7 +4114,7 @@ export class BaseClient { * // Output is 2 since the stream marked 2 entries as deleted. * ``` */ - public xdel(key: string, ids: string[]): Promise { + public async xdel(key: string, ids: string[]): Promise { return this.createWritePromise(createXDel(key, ids)); } @@ -4077,7 +4126,10 @@ export class BaseClient { * @param options - options detailing how to trim the stream. * @returns The number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. */ - public xtrim(key: string, options: StreamTrimOptions): Promise { + public async xtrim( + key: string, + options: StreamTrimOptions, + ): Promise { return this.createWritePromise(createXTrim(key, options)); } @@ -4104,7 +4156,7 @@ export class BaseClient { * // } * ``` */ - public xread( + public async xread( keys_and_ids: Record, options?: StreamReadOptions, ): Promise>> { @@ -4125,7 +4177,7 @@ export class BaseClient { * console.log(numEntries); // Output: 2 - "my_stream" contains 2 entries. * ``` */ - public xlen(key: string): Promise { + public async xlen(key: string): Promise { return this.createWritePromise(createXLen(key)); } @@ -4626,7 +4678,7 @@ export class BaseClient { * console.log(result); // Output: 'value3' - Returns the last element in the list stored at 'my_list'. * ``` */ - public lindex(key: string, index: number): Promise { + public async lindex(key: string, index: number): Promise { return this.createWritePromise(createLIndex(key, index)); } @@ -4650,7 +4702,7 @@ export class BaseClient { * console.log(length); // Output: 2 - The list has a length of 2 after performing the insert. * ``` */ - public linsert( + public async linsert( key: string, position: InsertPosition, pivot: string, @@ -4675,7 +4727,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates that the timeout associated with the key "my_key" was successfully removed. * ``` */ - public persist(key: string): Promise { + public async persist(key: string): Promise { return this.createWritePromise(createPersist(key)); } @@ -4697,7 +4749,7 @@ export class BaseClient { * console.log(result); // Output: OK - Indicates successful renaming of the key "old_key" to "new_key". * ``` */ - public rename(key: string, newKey: string): Promise<"OK"> { + public async rename(key: string, newKey: string): Promise<"OK"> { return this.createWritePromise(createRename(key, newKey)); } @@ -4719,7 +4771,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates successful renaming of the key "old_key" to "new_key". * ``` */ - public renamenx(key: string, newKey: string): Promise { + public async renamenx(key: string, newKey: string): Promise { return this.createWritePromise(createRenameNX(key, newKey)); } @@ -4744,7 +4796,7 @@ export class BaseClient { * console.log(result); // Output: ["list1", "element"] - Indicates an element "element" was popped from "list1". * ``` */ - public brpop( + public async brpop( keys: string[], timeout: number, ): Promise<[string, string] | null> { @@ -4771,7 +4823,7 @@ export class BaseClient { * console.log(result); // Output: ['list1', 'element'] * ``` */ - public blpop( + public async blpop( keys: string[], timeout: number, ): Promise<[string, string] | null> { @@ -4796,7 +4848,7 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that a new empty data structure was created * ``` */ - public pfadd(key: string, elements: string[]): Promise { + public async pfadd(key: string, elements: string[]): Promise { return this.createWritePromise(createPfAdd(key, elements)); } @@ -4815,7 +4867,7 @@ export class BaseClient { * console.log(result); // Output: 4 - The approximated cardinality of the union of "hll_1" and "hll_2" * ``` */ - public pfcount(keys: string[]): Promise { + public async pfcount(keys: string[]): Promise { return this.createWritePromise(createPfCount(keys)); } @@ -4860,7 +4912,7 @@ export class BaseClient { * console.log(result); // Output: "listpack" * ``` */ - public objectEncoding(key: string): Promise { + public async objectEncoding(key: string): Promise { return this.createWritePromise(createObjectEncoding(key)); } @@ -4877,7 +4929,7 @@ export class BaseClient { * console.log(result); // Output: 2 - The logarithmic access frequency counter of "my_hash". * ``` */ - public objectFreq(key: string): Promise { + public async objectFreq(key: string): Promise { return this.createWritePromise(createObjectFreq(key)); } @@ -4895,7 +4947,7 @@ export class BaseClient { * console.log(result); // Output: 13 - "my_hash" was last accessed 13 seconds ago. * ``` */ - public objectIdletime(key: string): Promise { + public async objectIdletime(key: string): Promise { return this.createWritePromise(createObjectIdletime(key)); } @@ -4914,7 +4966,7 @@ export class BaseClient { * console.log(result); // Output: 2 - "my_hash" has a reference count of 2. * ``` */ - public objectRefcount(key: string): Promise { + public async objectRefcount(key: string): Promise { return this.createWritePromise(createObjectRefcount(key)); } @@ -4938,7 +4990,7 @@ export class BaseClient { * console.log(response); // Output: Returns the function's return value. * ``` */ - public fcall( + public async fcall( func: string, keys: string[], args: string[], @@ -4967,7 +5019,7 @@ export class BaseClient { * console.log(response); // Output: 42 # The return value on the function that was executed. * ``` */ - public fcallReadonly( + public async fcallReadonly( func: string, keys: string[], args: string[], @@ -4997,7 +5049,7 @@ export class BaseClient { * console.log(await client.lpos("myList", "e", { count: 3 })); // Output: [ 4, 5 ] - indices for the occurrences of "e" in list "myList". * ``` */ - public lpos( + public async lpos( key: string, element: string, options?: LPosOptions, @@ -5025,7 +5077,10 @@ export class BaseClient { * console.log(await client.bitcount("my_key3", { start: -1, end: -1, indexType: BitmapIndexType.BIT })); // Output: 1 - Indicates that the last bit of the string stored at "my_key3" is set. * ``` */ - public bitcount(key: string, options?: BitOffsetOptions): Promise { + public async bitcount( + key: string, + options?: BitOffsetOptions, + ): Promise { return this.createWritePromise(createBitCount(key, options)); } @@ -5053,7 +5108,7 @@ export class BaseClient { * console.log(num); // Output: 1 - Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated. * ``` */ - public geoadd( + public async geoadd( key: string, membersToGeospatialData: Map, options?: GeoAddOptions, @@ -5242,7 +5297,7 @@ export class BaseClient { * console.log(result); // Output: [[13.36138933897018433, 38.11555639549629859], [15.08726745843887329, 37.50266842333162032], null] * ``` */ - public geopos( + public async geopos( key: string, members: string[], ): Promise<(number[] | null)[]> { @@ -5422,7 +5477,7 @@ export class BaseClient { * console.log(num); // Output: the distance between Place1 and Place2. * ``` */ - public geodist( + public async geodist( key: string, member1: string, member2: string, @@ -5449,7 +5504,10 @@ export class BaseClient { * console.log(num); // Output: ["sqc8b49rny0", "sqdtr74hyu0", null] * ``` */ - public geohash(key: string, members: string[]): Promise<(string | null)[]> { + public async geohash( + key: string, + members: string[], + ): Promise<(string | null)[]> { return this.createWritePromise<(string | null)[]>( createGeoHash(key, members), ).then((hashes) => diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 4dfb69d567..c0598c4360 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -148,7 +148,7 @@ export class GlideClient extends BaseClient { return configuration; } - public static createClient( + public static async createClient( options: GlideClientConfiguration, ): Promise { return super.createClientInternal( @@ -179,7 +179,7 @@ export class GlideClient extends BaseClient { * the list entry will be null. * If the transaction failed due to a WATCH command, `exec` will return `null`. */ - public exec( + public async exec( transaction: Transaction, decoder: Decoder = this.defaultDecoder, ): Promise { @@ -209,7 +209,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: Returns a list of all pub/sub clients * ``` */ - public customCommand( + public async customCommand( args: GlideString[], decoder?: Decoder, ): Promise { @@ -240,7 +240,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public ping(message?: string): Promise { + public async ping(message?: string): Promise { return this.createWritePromise(createPing(message)); } @@ -251,7 +251,7 @@ export class GlideClient extends BaseClient { * When no parameter is provided, the default option is assumed. * @returns a string containing the information for the sections requested. */ - public info(options?: InfoOptions[]): Promise { + public async info(options?: InfoOptions[]): Promise { return this.createWritePromise(createInfo(options)); } @@ -268,7 +268,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public select(index: number): Promise<"OK"> { + public async select(index: number): Promise<"OK"> { return this.createWritePromise(createSelect(index)); } @@ -284,7 +284,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'Client Name' * ``` */ - public clientGetName(): Promise { + public async clientGetName(): Promise { return this.createWritePromise(createClientGetName()); } @@ -300,7 +300,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configRewrite(): Promise<"OK"> { + public async configRewrite(): Promise<"OK"> { return this.createWritePromise(createConfigRewrite()); } @@ -316,7 +316,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configResetStat(): Promise<"OK"> { + public async configResetStat(): Promise<"OK"> { return this.createWritePromise(createConfigResetStat()); } @@ -325,7 +325,7 @@ export class GlideClient extends BaseClient { * * @returns the id of the client. */ - public clientId(): Promise { + public async clientId(): Promise { return this.createWritePromise(createClientId()); } @@ -343,7 +343,9 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: {'timeout': '1000', 'maxmemory': '1GB'} * ``` */ - public configGet(parameters: string[]): Promise> { + public async configGet( + parameters: string[], + ): Promise> { return this.createWritePromise(createConfigGet(parameters)); } @@ -361,7 +363,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configSet(parameters: Record): Promise<"OK"> { + public async configSet(parameters: Record): Promise<"OK"> { return this.createWritePromise(createConfigSet(parameters)); } @@ -378,7 +380,7 @@ export class GlideClient extends BaseClient { * console.log(echoedMessage); // Output: 'valkey-glide' * ``` */ - public echo(message: string): Promise { + public async echo(message: string): Promise { return this.createWritePromise(createEcho(message)); } @@ -396,7 +398,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: ['1710925775', '913580'] * ``` */ - public time(): Promise<[string, string]> { + public async time(): Promise<[string, string]> { return this.createWritePromise(createTime()); } @@ -476,7 +478,7 @@ export class GlideClient extends BaseClient { * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. * ``` */ - public lolwut(options?: LolwutOptions): Promise { + public async lolwut(options?: LolwutOptions): Promise { return this.createWritePromise(createLolwut(options)); } @@ -496,7 +498,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public functionDelete(libraryCode: string): Promise { + public async functionDelete(libraryCode: string): Promise { return this.createWritePromise(createFunctionDelete(libraryCode)); } @@ -519,7 +521,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'mylib' * ``` */ - public functionLoad( + public async functionLoad( libraryCode: string, replace?: boolean, ): Promise { @@ -544,7 +546,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public functionFlush(mode?: FlushMode): Promise { + public async functionFlush(mode?: FlushMode): Promise { return this.createWritePromise(createFunctionFlush(mode)); } @@ -667,7 +669,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public flushall(mode?: FlushMode): Promise { + public async flushall(mode?: FlushMode): Promise { return this.createWritePromise(createFlushAll(mode)); } @@ -685,7 +687,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public flushdb(mode?: FlushMode): Promise { + public async flushdb(mode?: FlushMode): Promise { return this.createWritePromise(createFlushDB(mode)); } @@ -702,7 +704,7 @@ export class GlideClient extends BaseClient { * console.log("Number of keys in the current database: ", numKeys); * ``` */ - public dbsize(): Promise { + public async dbsize(): Promise { return this.createWritePromise(createDBSize()); } @@ -722,7 +724,7 @@ export class GlideClient extends BaseClient { * 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 { + public async 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 479e6d87da..d6a3cbbbf4 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -356,7 +356,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: Returns a list of all pub/sub clients * ``` */ - public customCommand( + public async customCommand( args: GlideString[], options?: { route?: Routes; decoder?: Decoder }, ): Promise { @@ -379,7 +379,7 @@ export class GlideClusterClient extends BaseClient { * the list entry will be null. * If the transaction failed due to a WATCH command, `exec` will return `null`. */ - public exec( + public async exec( transaction: ClusterTransaction, options?: { route?: SingleNodeRoute; @@ -424,7 +424,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public ping(message?: string, route?: Routes): Promise { + public async ping(message?: string, route?: Routes): Promise { return this.createWritePromise(createPing(message), { route: toProtobufRoute(route), }); @@ -440,7 +440,7 @@ export class GlideClusterClient extends BaseClient { * @returns a string containing the information for the sections requested. When specifying a route other than a single node, * it returns a dictionary where each address is the key and its corresponding node response is the value. */ - public info( + public async info( options?: InfoOptions[], route?: Routes, ): Promise> { @@ -474,7 +474,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: {'addr': 'Connection Name', 'addr2': 'Connection Name', 'addr3': 'Connection Name'} * ``` */ - public clientGetName( + public async clientGetName( route?: Routes, ): Promise> { return this.createWritePromise>( @@ -498,7 +498,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configRewrite(route?: Routes): Promise<"OK"> { + public async configRewrite(route?: Routes): Promise<"OK"> { return this.createWritePromise(createConfigRewrite(), { route: toProtobufRoute(route), }); @@ -519,7 +519,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configResetStat(route?: Routes): Promise<"OK"> { + public async configResetStat(route?: Routes): Promise<"OK"> { return this.createWritePromise(createConfigResetStat(), { route: toProtobufRoute(route), }); @@ -533,7 +533,7 @@ export class GlideClusterClient extends BaseClient { * @returns the id of the client. When specifying a route other than a single node, * it returns a dictionary where each address is the key and its corresponding node response is the value. */ - public clientId(route?: Routes): Promise> { + public async clientId(route?: Routes): Promise> { return this.createWritePromise>( createClientId(), { route: toProtobufRoute(route) }, @@ -565,7 +565,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: {'timeout': '1000', 'maxmemory': '1GB'} * ``` */ - public configGet( + public async configGet( parameters: string[], route?: Routes, ): Promise>> { @@ -592,7 +592,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public configSet( + public async configSet( parameters: Record, route?: Routes, ): Promise<"OK"> { @@ -623,7 +623,7 @@ export class GlideClusterClient extends BaseClient { * console.log(echoedMessage); // Output: {'addr': 'valkey-glide', 'addr2': 'valkey-glide', 'addr3': 'valkey-glide'} * ``` */ - public echo( + public async echo( message: string, route?: Routes, ): Promise> { @@ -658,7 +658,9 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: {'addr': ['1710925775', '913580'], 'addr2': ['1710925775', '913580'], 'addr3': ['1710925775', '913580']} * ``` */ - public time(route?: Routes): Promise> { + public async time( + route?: Routes, + ): Promise> { return this.createWritePromise(createTime(), { route: toProtobufRoute(route), }); @@ -711,7 +713,7 @@ export class GlideClusterClient extends BaseClient { * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. * ``` */ - public lolwut( + public async lolwut( options?: LolwutOptions, route?: Routes, ): Promise> { @@ -740,7 +742,7 @@ export class GlideClusterClient extends BaseClient { * console.log(response); // Output: Returns the function's return value. * ``` */ - public fcallWithRoute( + public async fcallWithRoute( func: string, args: string[], route?: Routes, @@ -770,7 +772,7 @@ export class GlideClusterClient extends BaseClient { * console.log(response); // Output: 42 # The return value on the function that was execute. * ``` */ - public fcallReadonlyWithRoute( + public async fcallReadonlyWithRoute( func: string, args: string[], route?: Routes, @@ -798,7 +800,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public functionDelete( + public async functionDelete( libraryCode: string, route?: Routes, ): Promise { @@ -828,7 +830,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'mylib' * ``` */ - public functionLoad( + public async functionLoad( libraryCode: string, replace?: boolean, route?: Routes, @@ -857,7 +859,10 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public functionFlush(mode?: FlushMode, route?: Routes): Promise { + public async functionFlush( + mode?: FlushMode, + route?: Routes, + ): Promise { return this.createWritePromise(createFunctionFlush(mode), { route: toProtobufRoute(route), }); @@ -999,7 +1004,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public flushall(mode?: FlushMode, route?: Routes): Promise { + public async flushall(mode?: FlushMode, route?: Routes): Promise { return this.createWritePromise(createFlushAll(mode), { route: toProtobufRoute(route), }); @@ -1021,7 +1026,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public flushdb(mode?: FlushMode, route?: Routes): Promise { + public async flushdb(mode?: FlushMode, route?: Routes): Promise { return this.createWritePromise(createFlushDB(mode), { route: toProtobufRoute(route), }); @@ -1043,7 +1048,7 @@ export class GlideClusterClient extends BaseClient { * console.log("Number of keys across all primary nodes: ", numKeys); * ``` */ - public dbsize(route?: Routes): Promise> { + public async dbsize(route?: Routes): Promise> { return this.createWritePromise(createDBSize(), { route: toProtobufRoute(route), }); @@ -1075,7 +1080,7 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 2 - Published 2 instances of "Hi to sharded channel1!" message on channel1 using sharded mode * ``` */ - public publish( + public async publish( message: string, channel: string, sharded: boolean = false, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index d76ff0310a..4304a5a988 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -218,12 +218,12 @@ describe("GlideClusterClient", () => { client = await GlideClusterClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), ); - expect(() => + await expect( client.info(undefined, { type: "routeByAddress", host: "foo", }), - ).toThrowError(); + ).rejects.toThrowError(RequestError); }, TIMEOUT, ); From 8465f8ae723d548c1e64661bd763aad5fdf08f61 Mon Sep 17 00:00:00 2001 From: barshaul Date: Thu, 15 Aug 2024 09:17:16 +0000 Subject: [PATCH 173/236] Added default retries to the cluster client builder Signed-off-by: barshaul --- glide-core/src/client/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/glide-core/src/client/mod.rs b/glide-core/src/client/mod.rs index 249a409041..4dde234f86 100644 --- a/glide-core/src/client/mod.rs +++ b/glide-core/src/client/mod.rs @@ -23,7 +23,7 @@ mod value_conversion; use tokio::sync::mpsc; pub const HEARTBEAT_SLEEP_DURATION: Duration = Duration::from_secs(1); - +pub const DEFAULT_RETRIES: u32 = 3; pub const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_millis(250); pub const DEFAULT_CONNECTION_ATTEMPT_TIMEOUT: Duration = Duration::from_millis(250); pub const DEFAULT_PERIODIC_CHECKS_INTERVAL: Duration = Duration::from_secs(60); @@ -450,7 +450,8 @@ async fn create_cluster_client( None => Some(DEFAULT_PERIODIC_CHECKS_INTERVAL), }; let mut builder = redis::cluster::ClusterClientBuilder::new(initial_nodes) - .connection_timeout(INTERNAL_CONNECTION_TIMEOUT); + .connection_timeout(INTERNAL_CONNECTION_TIMEOUT) + .retries(DEFAULT_RETRIES); if read_from_replicas { builder = builder.read_from_replicas(); } From 4c25b82f408b20ec3566b35599713aaa55224af0 Mon Sep 17 00:00:00 2001 From: adarovadya Date: Thu, 15 Aug 2024 19:36:39 +0300 Subject: [PATCH 174/236] Node: change getdel, ping and get commands to bytes with Decoder (#2121) * change uint8 to Buffer * change getdel, ping and get commands to bytes * fix doc * add async Signed-off-by: Adar Ovadia --------- Signed-off-by: Adar Ovadia Co-authored-by: Adar Ovadia --- benchmarks/node/node_benchmark.ts | 11 ++++++++--- node/npm/glide/index.ts | 2 ++ node/rust-client/src/lib.rs | 6 +++--- node/src/BaseClient.ts | 24 +++++++++++++++++------- node/src/Commands.ts | 8 ++++---- node/src/GlideClient.ts | 12 +++++++++--- node/src/GlideClusterClient.ts | 12 +++++++++--- node/src/Transaction.ts | 10 +++++----- node/tests/SharedTests.ts | 22 +++++++++++++++++++++- node/tests/TestUtilities.ts | 5 +++-- 10 files changed, 81 insertions(+), 31 deletions(-) diff --git a/benchmarks/node/node_benchmark.ts b/benchmarks/node/node_benchmark.ts index 9c7e0b9772..a8ee7be6d8 100644 --- a/benchmarks/node/node_benchmark.ts +++ b/benchmarks/node/node_benchmark.ts @@ -8,7 +8,12 @@ import { parse } from "path"; import percentile from "percentile"; import { RedisClientType, createClient, createCluster } from "redis"; import { stdev } from "stats-lite"; -import { GlideClient, GlideClusterClient, Logger } from "valkey-glide"; +import { + GlideClient, + GlideClusterClient, + GlideString, + Logger, +} from "valkey-glide"; import { generateKeyGet, generateKeySet, @@ -34,8 +39,8 @@ const runningTasks: Promise[] = []; const benchJsonResults: object[] = []; interface IAsyncClient { - set: (key: string, value: string) => Promise; - get: (key: string) => Promise; + set: (key: string, value: string) => Promise; + get: (key: string) => Promise; } function chooseAction(): ChosenAction { diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 827d93ee45..4cf1855926 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -104,6 +104,7 @@ function initialize() { GlideClient, GlideClusterClient, GlideClientConfiguration, + GlideString, FunctionListOptions, FunctionListResponse, FunctionStatsResponse, @@ -185,6 +186,7 @@ function initialize() { BitwiseOperation, ConditionalChange, GeoAddOptions, + GlideString, CoordOrigin, MemberOrigin, SearchOrigin, diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index c301e082de..896478f76a 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -76,7 +76,7 @@ impl AsyncClient { }) } - #[napi(ts_return_type = "Promise")] + #[napi(ts_return_type = "Promise")] #[allow(dead_code)] pub fn get(&self, env: Env, key: String) -> Result { let (deferred, promise) = env.create_deferred()?; @@ -93,7 +93,7 @@ impl AsyncClient { Ok(promise) } - #[napi(ts_return_type = "Promise")] + #[napi(ts_return_type = "Promise")] #[allow(dead_code)] pub fn set(&self, env: Env, key: String, value: String) -> Result { let (deferred, promise) = env.create_deferred()?; @@ -263,7 +263,7 @@ fn redis_value_to_js(val: Value, js_env: Env, string_decoder: bool) -> Result { + public async get( + key: GlideString, + decoder?: Decoder, + ): Promise { return this.createWritePromise(createGet(key), { decoder: decoder }); } @@ -968,6 +974,7 @@ export class BaseClient { * See https://valkey.io/commands/getdel/ for details. * * @param key - The key to retrieve from the database. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns If `key` exists, returns the `value` of `key`. Otherwise, return `null`. * * @example @@ -978,8 +985,11 @@ export class BaseClient { * const value = client.getdel("key"); // value is null * ``` */ - public async getdel(key: string): Promise { - return this.createWritePromise(createGetDel(key)); + public async getdel( + key: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createGetDel(key), { decoder: decoder }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index dee2022c9d..209a896cd8 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -92,14 +92,14 @@ function createCommand( /** * @internal */ -export function createGet(key: string): command_request.Command { +export function createGet(key: GlideString): command_request.Command { return createCommand(RequestType.Get, [key]); } /** * @internal */ -export function createGetDel(key: string): command_request.Command { +export function createGetDel(key: GlideString): command_request.Command { return createCommand(RequestType.GetDel, [key]); } @@ -269,8 +269,8 @@ export enum InfoOptions { /** * @internal */ -export function createPing(str?: string): command_request.Command { - const args: string[] = str == undefined ? [] : [str]; +export function createPing(str?: GlideString): command_request.Command { + const args: GlideString[] = str == undefined ? [] : [str]; return createCommand(RequestType.Ping, args); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index c0598c4360..ce34cbec70 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -173,7 +173,7 @@ export class GlideClient extends BaseClient { * See https://redis.io/topics/Transactions/ for details on Redis Transactions. * * @param transaction - A Transaction object containing a list of commands to be executed. - * @param decoder - An optional parameter to decode all commands in the transaction. If not set, 'Decoder.String' will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the responses. If not set, the default decoder from the client config will be used. * @returns A list of results corresponding to the execution of each command in the transaction. * If a command returns a value, it will be included in the list. If a command doesn't return a value, * the list entry will be null. @@ -224,6 +224,7 @@ export class GlideClient extends BaseClient { * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". * If provided, the server will respond with a copy of the message. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. * * @example @@ -240,8 +241,13 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public async ping(message?: string): Promise { - return this.createWritePromise(createPing(message)); + public async ping(options?: { + message?: GlideString; + decoder?: Decoder; + }): Promise { + return this.createWritePromise(createPing(options?.message), { + decoder: options?.decoder, + }); } /** Get information and statistics about the Redis server. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d6a3cbbbf4..84f875355b 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -408,6 +408,7 @@ export class GlideClusterClient extends BaseClient { * If provided, the server will respond with a copy of the message. * @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`. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. * * @example @@ -424,9 +425,14 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public async ping(message?: string, route?: Routes): Promise { - return this.createWritePromise(createPing(message), { - route: toProtobufRoute(route), + public async ping(options?: { + message?: GlideString; + route?: Routes; + decoder?: Decoder; + }): Promise { + return this.createWritePromise(createPing(options?.message), { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2d90524dd9..7ac86a8f48 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -58,6 +58,7 @@ import { StreamPendingOptions, StreamReadOptions, StreamTrimOptions, + TimeUnit, ZAddOptions, createAppend, createBLMPop, @@ -242,7 +243,6 @@ import { createZRevRankWithScore, createZScan, createZScore, - TimeUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -301,9 +301,9 @@ export class BaseTransaction> { * * @param key - The key to retrieve from the database. * - * Command Response - If `key` exists, returns the value of `key` as a string. Otherwise, return null. + * Command Response - If `key` exists, returns the value of `key`. Otherwise, return null. */ - public get(key: string): T { + public get(key: GlideString): T { return this.addAndReturn(createGet(key)); } @@ -336,7 +336,7 @@ export class BaseTransaction> { * * Command Response - If `key` exists, returns the `value` of `key`. Otherwise, return `null`. */ - public getdel(key: string): T { + public getdel(key: GlideString): T { return this.addAndReturn(createGetDel(key)); } @@ -383,7 +383,7 @@ export class BaseTransaction> { * * Command Response - "PONG" if `message` is not provided, otherwise return a copy of `message`. */ - public ping(message?: string): T { + public ping(message?: GlideString): T { return this.addAndReturn(createPing(message)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0936117791..d9f3d2129a 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -472,8 +472,21 @@ export function runBaseTests(config: { `ping test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { + const pongEncoded = Buffer.from("PONG"); + const helloEncoded = Buffer.from("Hello"); expect(await client.ping()).toEqual("PONG"); - expect(await client.ping("Hello")).toEqual("Hello"); + expect(await client.ping({ message: "Hello" })).toEqual( + "Hello", + ); + expect(await client.ping({ decoder: Decoder.Bytes })).toEqual( + pongEncoded, + ); + expect( + await client.ping({ + message: "Hello", + decoder: Decoder.Bytes, + }), + ).toEqual(helloEncoded); }, protocol); }, config.timeout, @@ -1177,12 +1190,19 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); const value1 = uuidv4(); + const value1Encoded = Buffer.from(value1); const key2 = uuidv4(); expect(await client.set(key1, value1)).toEqual("OK"); expect(await client.getdel(key1)).toEqual(value1); expect(await client.getdel(key1)).toEqual(null); + expect(await client.set(key1, value1)).toEqual("OK"); + expect(await client.getdel(key1, Decoder.Bytes)).toEqual( + value1Encoded, + ); + expect(await client.getdel(key1, Decoder.Bytes)).toEqual(null); + // key isn't a string expect(await client.sadd(key2, ["a"])).toEqual(1); await expect(client.getdel(key2)).rejects.toThrow(RequestError); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d90283399c..7e704667a1 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -24,6 +24,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + GlideString, InfBoundary, InsertPosition, ListDirection, @@ -130,8 +131,8 @@ export function checkSimple(left: any): Checker { } export type Client = { - set: (key: string, value: string) => Promise; - get: (key: string) => Promise; + set: (key: string, value: string) => Promise; + get: (key: string) => Promise; }; export async function GetAndSetRandomValue(client: Client) { From 65fcb42d15cb40700581f0b338dfe857ece945b4 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 15 Aug 2024 09:56:11 -0700 Subject: [PATCH 175/236] Node: Clean `since` tags in command documentation (#2111) * Update Node documentation since and see tagging. Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- node/src/BaseClient.ts | 609 +++++++++++++++++---------------- node/src/GlideClient.ts | 103 +++--- node/src/GlideClusterClient.ts | 117 +++---- node/src/Transaction.ts | 561 ++++++++++++++---------------- 4 files changed, 684 insertions(+), 706 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 51e9c578ce..b43e37e50d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -299,12 +299,12 @@ class PointerResponse { /** Represents the credentials for connecting to a server. */ export type RedisCredentials = { /** - * The username that will be used for authenticating connections to the Redis servers. + * The username that will be used for authenticating connections to the Valkey servers. * If not supplied, "default" will be used. */ username?: string; /** - * The password that will be used for authenticating connections to the Redis servers. + * The password that will be used for authenticating connections to the Valkey servers. */ password: string; }; @@ -919,7 +919,8 @@ export class BaseClient { } /** Get the value associated with the given key, or null if no such value exists. - * See https://valkey.io/commands/get/ for details. + * + * @see {@link https://valkey.io/commands/get/|valkey.io} for details. * * @param key - The key to retrieve from the database. * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. @@ -945,7 +946,8 @@ export class BaseClient { /** * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. * - * See https://valkey.io/commands/getex for more details. + * @see {@link https://valkey.io/commands/getex/|valkey.op} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key to retrieve from the database. * @param options - (Optional) Set expiriation to the given key. @@ -953,8 +955,6 @@ export class BaseClient { * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. * @returns If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. * - * since - Valkey 6.2.0 and above. - * * @example * ```typescript * const result = await client.getex("key", {expiry: { type: TimeUnit.Seconds, count: 5 }}); @@ -971,7 +971,7 @@ export class BaseClient { /** * Gets a string value associated with the given `key`and deletes the key. * - * See https://valkey.io/commands/getdel/ for details. + * @see {@link https://valkey.io/commands/getdel/|valkey.io} for details. * * @param key - The key to retrieve from the database. * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. @@ -999,7 +999,7 @@ export class BaseClient { * penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` * or `end` are out of range, returns the substring within the valid range of the string. * - * See https://valkey.io/commands/getrange/ for details. + * @see {@link https://valkey.io/commands/getrange/|valkey.io} for details. * * @param key - The key of the string. * @param start - The starting offset. @@ -1028,7 +1028,8 @@ export class BaseClient { } /** Set the given key with the given value. Return value is dependent on the passed options. - * See https://valkey.io/commands/set/ for details. + * + * @see {@link https://valkey.io/commands/set/|valkey.io} for details. * * @param key - The key to store. * @param value - The value to store with the given key. @@ -1065,7 +1066,8 @@ export class BaseClient { } /** Removes the specified keys. A key is ignored if it does not exist. - * See https://valkey.io/commands/del/ for details. + * + * @see {@link https://valkey.io/commands/del/|valkey.io} for details. * * @param keys - the keys we wanted to remove. * @returns the number of keys that were removed. @@ -1090,9 +1092,10 @@ export class BaseClient { } /** Retrieve the values of multiple keys. - * See https://valkey.io/commands/mget/ for details. * + * @see {@link https://valkey.io/commands/mget/|valkey.io} for details. * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + * * @param keys - A list of keys to retrieve values for. * @returns A list of values corresponding to the provided keys. If a key is not found, * its corresponding value in the list will be null. @@ -1111,9 +1114,10 @@ export class BaseClient { } /** Set multiple keys to multiple values in a single operation. - * See https://valkey.io/commands/mset/ for details. * + * @see {@link https://valkey.io/commands/mset/|valkey.io} for details. * @remarks When in cluster mode, the command may route to multiple nodes when keys in `keyValueMap` map to different hash slots. + * * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * @returns always "OK". * @@ -1132,9 +1136,9 @@ export class BaseClient { * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or * more keys already exist, the entire operation fails. * - * See https://valkey.io/commands/msetnx/ for more details. - * + * @see {@link https://valkey.io/commands/msetnx/|valkey.io} for more details. * @remarks When in cluster mode, all keys in `keyValueMap` must map to the same hash slot. + * * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * @returns `true` if all keys were set. `false` if no key was set. * @@ -1152,7 +1156,8 @@ export class BaseClient { } /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incr/ for details. + * + * @see {@link https://valkey.io/commands/incr/|valkey.io} for details. * * @param key - The key to increment its value. * @returns the value of `key` after the increment. @@ -1170,7 +1175,8 @@ export class BaseClient { } /** Increments the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incrby/ for details. + * + * @see {@link https://valkey.io/commands/incrby/|valkey.io} for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -1191,7 +1197,8 @@ export class BaseClient { /** Increment the string representing a floating point number stored at `key` by `amount`. * By using a negative increment value, the result is that the value stored at `key` is decremented. * If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incrbyfloat/ for details. + * + * @see {@link https://valkey.io/commands/incrbyfloat/|valkey.io} for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -1210,7 +1217,8 @@ export class BaseClient { } /** Decrements the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/decr/ for details. + * + * @see {@link https://valkey.io/commands/decr/|valkey.io} for details. * * @param key - The key to decrement its value. * @returns the value of `key` after the decrement. @@ -1228,7 +1236,8 @@ export class BaseClient { } /** Decrements the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/decrby/ for details. + * + * @see {@link https://valkey.io/commands/decrby/|valkey.io} for details. * * @param key - The key to decrement its value. * @param amount - The amount to decrement. @@ -1250,9 +1259,9 @@ export class BaseClient { * Perform a bitwise operation between multiple keys (containing string values) and store the result in the * `destination`. * - * See https://valkey.io/commands/bitop/ for more details. - * + * @see {@link https://valkey.io/commands/bitop/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * * @param operation - The bitwise operation to perform. * @param destination - The key that will store the resulting string. * @param keys - The list of keys to perform the bitwise operation on. @@ -1283,7 +1292,7 @@ export class BaseClient { * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. * - * See https://valkey.io/commands/getbit/ for more details. + * @see {@link https://valkey.io/commands/getbit/|valkey.io} for more details. * * @param key - The key of the string. * @param offset - The index of the bit to return. @@ -1306,7 +1315,7 @@ export class BaseClient { * `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to `value` and * the preceding bits are set to `0`. * - * See https://valkey.io/commands/setbit/ for more details. + * @see {@link https://valkey.io/commands/setbit/|valkey.io} for more details. * * @param key - The key of the string. * @param offset - The index of the bit to be set. @@ -1333,7 +1342,7 @@ export class BaseClient { * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being * the last byte of the list, `-2` being the penultimate, and so on. * - * See https://valkey.io/commands/bitpos/ for more details. + * @see {@link https://valkey.io/commands/bitpos/|valkey.io} for more details. * * @param key - The key of the string. * @param bit - The bit value to match. Must be `0` or `1`. @@ -1370,7 +1379,7 @@ export class BaseClient { * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is * specified, `start=0` and `end=2` means to look at the first three bytes. * - * See https://valkey.io/commands/bitpos/ for more details. + * @see {@link https://valkey.io/commands/bitpos/|valkey.io} for more details. * * @param key - The key of the string. * @param bit - The bit value to match. Must be `0` or `1`. @@ -1408,7 +1417,7 @@ export class BaseClient { * Reads or modifies the array of bits representing the string that is held at `key` based on the specified * `subcommands`. * - * See https://valkey.io/commands/bitfield/ for more details. + * @see {@link https://valkey.io/commands/bitfield/|valkey.io} for more details. * * @param key - The key of the string. * @param subcommands - The subcommands to be performed on the binary value of the string at `key`, which could be @@ -1445,14 +1454,13 @@ export class BaseClient { /** * Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. * - * See https://valkey.io/commands/bitfield_ro/ for more details. + * @see {@link https://valkey.io/commands/bitfield_ro/|valkey.io} for more details. + * @remarks Since Valkey version 6.0.0. * * @param key - The key of the string. * @param subcommands - The {@link BitFieldGet} subcommands to be performed. * @returns An array of results from the {@link BitFieldGet} subcommands. * - * since Valkey version 6.0.0. - * * @example * ```typescript * await client.set("key", "A"); // "A" has binary value 01000001 @@ -1468,7 +1476,8 @@ export class BaseClient { } /** Retrieve the value associated with `field` in the hash stored at `key`. - * See https://valkey.io/commands/hget/ for details. + * + * @see {@link https://valkey.io/commands/hget/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. @@ -1494,7 +1503,8 @@ export class BaseClient { } /** Sets the specified fields to their respective values in the hash stored at `key`. - * See https://valkey.io/commands/hset/ for details. + * + * @see {@link https://valkey.io/commands/hset/|valkey.io} for details. * * @param key - The key of the hash. * @param fieldValueMap - A field-value map consisting of fields and their corresponding values @@ -1518,7 +1528,8 @@ export class BaseClient { /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. - * See https://valkey.io/commands/hsetnx/ for more details. + * + * @see {@link https://valkey.io/commands/hsetnx/|valkey.io} for more details. * * @param key - The key of the hash. * @param field - The field to set the value for. @@ -1549,7 +1560,8 @@ export class BaseClient { /** Removes the specified fields from the hash stored at `key`. * Specified fields that do not exist within this hash are ignored. - * See https://valkey.io/commands/hdel/ for details. + * + * @see {@link https://valkey.io/commands/hdel/|valkey.io} for details. * * @param key - The key of the hash. * @param fields - The fields to remove from the hash stored at `key`. @@ -1568,7 +1580,8 @@ export class BaseClient { } /** Returns the values associated with the specified fields in the hash stored at `key`. - * See https://valkey.io/commands/hmget/ for details. + * + * @see {@link https://valkey.io/commands/hmget/|valkey.io} for details. * * @param key - The key of the hash. * @param fields - The fields in the hash stored at `key` to retrieve from the database. @@ -1591,7 +1604,8 @@ export class BaseClient { } /** Returns if `field` is an existing field in the hash stored at `key`. - * See https://valkey.io/commands/hexists/ for details. + * + * @see {@link https://valkey.io/commands/hexists/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field to check in the hash stored at `key`. @@ -1616,7 +1630,8 @@ export class BaseClient { } /** Returns all fields and values of the hash stored at `key`. - * See https://valkey.io/commands/hgetall/ for details. + * + * @see {@link https://valkey.io/commands/hgetall/|valkey.io} for details. * * @param key - The key of the hash. * @returns a list of fields and their values stored in the hash. Every field name in the list is followed by its value. @@ -1636,7 +1651,8 @@ export class BaseClient { /** Increments the number stored at `field` in the hash stored at `key` by increment. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/hincrby/ for details. + * + * @see {@link https://valkey.io/commands/hincrby/|valkey.io} for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -1661,7 +1677,8 @@ export class BaseClient { /** Increment the string representing a floating point number stored at `field` in the hash stored at `key` by increment. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/hincrbyfloat/ for details. + * + * @see {@link https://valkey.io/commands/hincrbyfloat/|valkey.io} for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -1684,7 +1701,8 @@ export class BaseClient { } /** Returns the number of fields contained in the hash stored at `key`. - * See https://valkey.io/commands/hlen/ for more details. + * + * @see {@link https://valkey.io/commands/hlen/|valkey.io} for more details. * * @param key - The key of the hash. * @returns The number of fields in the hash, or 0 when the key does not exist. @@ -1708,7 +1726,8 @@ export class BaseClient { } /** Returns all values in the hash stored at key. - * See https://valkey.io/commands/hvals/ for more details. + * + * @see {@link https://valkey.io/commands/hvals/|valkey.io} for more details. * * @param key - The key of the hash. * @returns a list of values in the hash, or an empty list when the key does not exist. @@ -1727,7 +1746,7 @@ export class BaseClient { /** * Returns the string length of the value associated with `field` in the hash stored at `key`. * - * See https://valkey.io/commands/hstrlen/ for details. + * @see {@link https://valkey.io/commands/hstrlen/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field in the hash. @@ -1747,9 +1766,8 @@ export class BaseClient { /** * Returns a random field name from the hash value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @returns A random field name from the hash stored at `key`, or `null` when @@ -1767,7 +1785,7 @@ export class BaseClient { /** * Iterates incrementally over a hash. * - * See https://valkey.io/commands/hscan for more details. + * @see {@link https://valkey.io/commands/hscan/|valkey.io} for more details. * * @param key - The key of the set. * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. @@ -1812,9 +1830,8 @@ export class BaseClient { /** * Retrieves up to `count` random field names from the hash value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @param count - The number of field names to return. @@ -1839,9 +1856,8 @@ export class BaseClient { * Retrieves up to `count` random field names along with their values from the hash * value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @param count - The number of field names to return. @@ -1867,7 +1883,8 @@ export class BaseClient { /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://valkey.io/commands/lpush/ for details. + * + * @see {@link https://valkey.io/commands/lpush/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -1895,7 +1912,7 @@ export class BaseClient { * 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. + * @see {@link https://valkey.io/commands/lpushx/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -1912,7 +1929,8 @@ export class BaseClient { /** 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. + * + * @see {@link https://valkey.io/commands/lpop/|valkey.io} for details. * * @param key - The key of the list. * @returns The value of the first element. @@ -1937,7 +1955,8 @@ export class BaseClient { } /** Removes and returns up to `count` elements of the list stored at `key`, depending on the list's length. - * See https://valkey.io/commands/lpop/ for details. + * + * @see {@link https://valkey.io/commands/lpop/|valkey.io} for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -1969,7 +1988,8 @@ export class BaseClient { * 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, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://valkey.io/commands/lrange/ for details. + * + * @see {@link https://valkey.io/commands/lrange/|valkey.io} for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -2009,7 +2029,8 @@ export class BaseClient { } /** Returns the length of the list stored at `key`. - * See https://valkey.io/commands/llen/ for details. + * + * @see {@link https://valkey.io/commands/llen/|valkey.io} for details. * * @param key - The key of the list. * @returns the length of the list at `key`. @@ -2031,7 +2052,8 @@ export class BaseClient { * depending on `whereTo`, and pushes the element at the first/last element of the list * stored at `destination` depending on `whereFrom`, see {@link ListDirection}. * - * See https://valkey.io/commands/lmove/ for details. + * @see {@link https://valkey.io/commands/lmove/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source list. * @param destination - The key to the destination list. @@ -2039,8 +2061,6 @@ export class BaseClient { * @param whereTo - The {@link ListDirection} to add the element to. * @returns The popped element, or `null` if `source` does not exist. * - * since Valkey version 6.2.0. - * * @example * ```typescript * await client.lpush("testKey1", ["two", "one"]); @@ -2073,11 +2093,10 @@ export class BaseClient { * of the list stored at `destination` depending on `whereTo`. * `BLMOVE` is the blocking variant of {@link lmove}. * - * @remarks - * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. - * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. - * - * See https://valkey.io/commands/blmove/ for details. + * @see {@link https://valkey.io/commands/blmove/|valkey.io} for details. + * @remarks When in cluster mode, both `source` and `destination` must map to the same hash slot. + * @remarks `BLMOVE` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands|Valkey Glide Wiki} for more details and best practices. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source list. * @param destination - The key to the destination list. @@ -2086,8 +2105,6 @@ export class BaseClient { * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. * - * since Valkey version 6.2.0. - * * @example * ```typescript * await client.lpush("testKey1", ["two", "one"]); @@ -2120,7 +2137,7 @@ export class BaseClient { * 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. + * @see {@link https://valkey.io/commands/lset/|valkey.io} for details. * * @param key - The key of the list. * @param index - The index of the element in the list to be set. @@ -2146,7 +2163,8 @@ export class BaseClient { * 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, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://valkey.io/commands/ltrim/ for details. + * + * @see {@link https://valkey.io/commands/ltrim/|valkey.io} for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -2196,7 +2214,8 @@ export class BaseClient { /** Inserts all the specified values at the tail of the list stored at `key`. * `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://valkey.io/commands/rpush/ for details. + * + * @see {@link https://valkey.io/commands/rpush/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -2224,7 +2243,7 @@ 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. + * @see {@link https://valkey.io/commands/rpushx/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -2241,7 +2260,8 @@ export class BaseClient { /** 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. + * + * @see {@link https://valkey.io/commands/rpop/|valkey.io} for details. * * @param key - The key of the list. * @returns The value of the last element. @@ -2266,7 +2286,8 @@ export class BaseClient { } /** Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - * See https://valkey.io/commands/rpop/ for details. + * + * @see {@link https://valkey.io/commands/rpop/|valkey.io} for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -2296,7 +2317,8 @@ export class BaseClient { /** Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. * If `key` does not exist, a new set is created before adding `members`. - * See https://valkey.io/commands/sadd/ for details. + * + * @see {@link https://valkey.io/commands/sadd/|valkey.io} for details. * * @param key - The key to store the members to its set. * @param members - A list of members to add to the set stored at `key`. @@ -2314,7 +2336,8 @@ export class BaseClient { } /** Removes the specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - * See https://valkey.io/commands/srem/ for details. + * + * @see {@link https://valkey.io/commands/srem/|valkey.io} for details. * * @param key - The key to remove the members from its set. * @param members - A list of members to remove from the set stored at `key`. @@ -2333,7 +2356,8 @@ export class BaseClient { } /** Returns all the members of the set value stored at `key`. - * See https://valkey.io/commands/smembers/ for details. + * + * @see {@link https://valkey.io/commands/smembers/|valkey.io} for details. * * @param key - The key to return its members. * @returns A `Set` containing all members of the set. @@ -2354,8 +2378,8 @@ export class BaseClient { /** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. * Creates a new destination set if needed. The operation is atomic. - * See https://valkey.io/commands/smove for more details. * + * @see {@link https://valkey.io/commands/smove/|valkey.io} for more details. * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. * * @param source - The key of the set to remove the element from. @@ -2380,7 +2404,8 @@ export class BaseClient { } /** Returns the set cardinality (number of elements) of the set stored at `key`. - * See https://valkey.io/commands/scard/ for details. + * + * @see {@link https://valkey.io/commands/scard/|valkey.io} for details. * * @param key - The key to return the number of its members. * @returns The cardinality (number of elements) of the set, or 0 if key does not exist. @@ -2397,9 +2422,10 @@ export class BaseClient { } /** Gets the intersection of all the given sets. - * See https://valkey.io/docs/latest/commands/sinter/ for more details. * + * @see {@link https://valkey.io/docs/latest/commands/sinter/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * * @param keys - The `keys` of the sets to get the intersection. * @returns - A set of members which are present in all given sets. * If one or more sets do not exist, an empty set will be returned. @@ -2427,15 +2453,14 @@ export class BaseClient { /** * Gets the cardinality of the intersection of all the given sets. * - * See https://valkey.io/commands/sintercard/ for more details. - * + * @see {@link https://valkey.io/commands/sintercard/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param keys - The keys of the sets. * @param limit - The limit for the intersection cardinality value. If not specified, or set to `0`, no limit is used. * @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"]); @@ -2454,9 +2479,9 @@ export class BaseClient { /** * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. * - * See https://valkey.io/commands/sinterstore/ for more details. - * + * @see {@link https://valkey.io/commands/sinterstore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * * @param destination - The key of the destination set. * @param keys - The keys from which to retrieve the set members. * @returns The number of elements in the resulting set. @@ -2477,9 +2502,9 @@ export class BaseClient { /** * Computes the difference between the first set and all the successive sets in `keys`. * - * See https://valkey.io/commands/sdiff/ for more details. - * + * @see {@link https://valkey.io/commands/sdiff/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * * @param keys - The keys of the sets to diff. * @returns A `Set` of elements representing the difference between the sets. * If a key in `keys` does not exist, it is treated as an empty set. @@ -2501,9 +2526,9 @@ export class BaseClient { /** * Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. * - * See https://valkey.io/commands/sdiffstore/ for more details. - * + * @see {@link https://valkey.io/commands/sdiffstore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * * @param destination - The key of the destination set. * @param keys - The keys of the sets to diff. * @returns The number of elements in the resulting set. @@ -2526,9 +2551,9 @@ export class BaseClient { /** * Gets the union of all the given sets. * - * See https://valkey.io/commands/sunion/ for more details. - * + * @see {@link https://valkey.io/commands/sunion/|valkey.io} 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 A `Set` of members which are present in at least one of the given sets. * If none of the sets exist, an empty `Set` will be returned. @@ -2554,9 +2579,9 @@ export class BaseClient { * Stores the members of the union of all given sets specified by `keys` into a new set * at `destination`. * - * See https://valkey.io/commands/sunionstore/ for details. - * + * @see {@link https://valkey.io/commands/sunionstore/|valkey.io} for details. * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * * @param destination - The key of the destination set. * @param keys - The keys from which to retrieve the set members. * @returns The number of elements in the resulting set. @@ -2575,7 +2600,8 @@ export class BaseClient { } /** Returns if `member` is a member of the set stored at `key`. - * See https://valkey.io/commands/sismember/ for more details. + * + * @see {@link https://valkey.io/commands/sismember/|valkey.io} for more details. * * @param key - The key of the set. * @param member - The member to check for existence in the set. @@ -2603,14 +2629,13 @@ export class BaseClient { /** * Checks whether each member is contained in the members of the set stored at `key`. * - * See https://valkey.io/commands/smismember/ for more details. + * @see {@link https://valkey.io/commands/smismember/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @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"]); @@ -2626,8 +2651,9 @@ export class BaseClient { } /** 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`. + * To pop multiple members, see {@link spopCount}. + * + * @see {@link https://valkey.io/commands/spop/|valkey.io} for details. * * @param key - The key of the set. * @returns the value of the popped member. @@ -2652,7 +2678,8 @@ export class BaseClient { } /** Removes and returns up to `count` random members from the set value store at `key`, depending on the set's length. - * See https://valkey.io/commands/spop/ for details. + * + * @see {@link https://valkey.io/commands/spop/|valkey.io} for details. * * @param key - The key of the set. * @param count - The count of the elements to pop from the set. @@ -2682,7 +2709,7 @@ export class BaseClient { /** * Returns a random element from the set value stored at `key`. * - * See https://valkey.io/commands/srandmember for more details. + * @see {@link https://valkey.io/commands/srandmember/|valkey.io} for more details. * * @param key - The key from which to retrieve the set member. * @returns A random element from the set, or null if `key` does not exist. @@ -2708,7 +2735,7 @@ export class BaseClient { /** * Returns one or more random elements from the set value stored at `key`. * - * See https://valkey.io/commands/srandmember for more details. + * @see {@link https://valkey.io/commands/srandmember/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - The number of members to return. @@ -2738,7 +2765,8 @@ export class BaseClient { } /** Returns the number of keys in `keys` that exist in the database. - * See https://valkey.io/commands/exists/ for details. + * + * @see {@link https://valkey.io/commands/exists/|valkey.io} for details. * * @param keys - The keys list to check. * @returns The number of keys that exist. If the same existing key is mentioned in `keys` multiple times, @@ -2758,7 +2786,8 @@ export class BaseClient { /** Removes the specified keys. A key is ignored if it does not exist. * This command, similar to DEL, removes specified keys and ignores non-existent ones. * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. - * See https://valkey.io/commands/unlink/ for details. + * + * @see {@link https://valkey.io/commands/unlink/|valkey.io} for details. * * @param keys - The keys we wanted to unlink. * @returns The number of keys that were unlinked. @@ -2778,7 +2807,8 @@ export class BaseClient { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/expire/ for details. + * + * @see {@link https://valkey.io/commands/expire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. @@ -2812,7 +2842,8 @@ export class BaseClient { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/expireat/ for details. + * + * @see {@link https://valkey.io/commands/expireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. @@ -2841,13 +2872,12 @@ export class BaseClient { * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. * To get the expiration with millisecond precision, use {@link pexpiretime}. * - * See https://valkey.io/commands/expiretime/ for details. + * @see {@link https://valkey.io/commands/expiretime/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since Valkey version 7.0.0. - * * @example * ```typescript * const result1 = await client.expiretime("myKey"); @@ -2870,7 +2900,8 @@ export class BaseClient { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/pexpire/ for details. + * + * @see {@link https://valkey.io/commands/pexpire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. @@ -2899,7 +2930,8 @@ export class BaseClient { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/pexpireat/ for details. + * + * @see {@link https://valkey.io/commands/pexpireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. @@ -2927,13 +2959,12 @@ export class BaseClient { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. * - * See https://valkey.io/commands/pexpiretime/ for details. + * @see {@link https://valkey.io/commands/pexpiretime/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since Valkey version 7.0.0. - * * @example * ```typescript * const result1 = client.pexpiretime("myKey"); @@ -2953,7 +2984,8 @@ export class BaseClient { } /** Returns the remaining time to live of `key` that has a timeout. - * See https://valkey.io/commands/ttl/ for details. + * + * @see {@link https://valkey.io/commands/ttl/|valkey.io} for details. * * @param key - The key to return its timeout. * @returns TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. @@ -2984,10 +3016,11 @@ export class BaseClient { } /** Invokes a Lua script with its keys and arguments. - * This method simplifies the process of invoking scripts on a Redis server by using an object that represents a Lua script. + * This method simplifies the process of invoking scripts on a Valkey server by using an object that represents a Lua script. * The script loading, argument preparation, and execution will all be handled internally. If the script has not already been loaded, - * it will be loaded automatically using the Redis `SCRIPT LOAD` command. After that, it will be invoked using the Redis `EVALSHA` command - * See https://valkey.io/commands/script-load/ and https://valkey.io/commands/evalsha/ for details. + * it will be loaded automatically using the `SCRIPT LOAD` command. After that, it will be invoked using the `EVALSHA` command. + * + * @see {@link https://valkey.io/commands/script-load/|SCRIPT LOAD} and {@link https://valkey.io/commands/evalsha/|EVALSHA} on valkey.io for details. * * @param script - The Lua script to execute. * @param options - The script option that contains keys and arguments for the script. @@ -3035,7 +3068,7 @@ export class BaseClient { /** * Returns stream entries matching a given range of entry IDs. * - * See https://valkey.io/commands/xrange for more details. + * @see {@link https://valkey.io/commands/xrange/|valkey.io} for more details. * * @param key - The key of the stream. * @param start - The starting stream entry ID bound for the range. @@ -3073,7 +3106,8 @@ export class BaseClient { /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. - * See https://valkey.io/commands/zadd/ for more details. + * + * @see {@link https://valkey.io/commands/zadd/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. @@ -3109,7 +3143,8 @@ export class BaseClient { /** Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. - * See https://valkey.io/commands/zadd/ for more details. + * + * @see {@link https://valkey.io/commands/zadd/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. @@ -3145,7 +3180,8 @@ export class BaseClient { /** Removes the specified members from the sorted set stored at `key`. * Specified members that are not a member of this set are ignored. - * See https://valkey.io/commands/zrem/ for more details. + * + * @see {@link https://valkey.io/commands/zrem/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param members - A list of members to remove from the sorted set. @@ -3171,7 +3207,8 @@ export class BaseClient { } /** Returns the cardinality (number of elements) of the sorted set stored at `key`. - * See https://valkey.io/commands/zcard/ for more details. + * + * @see {@link https://valkey.io/commands/zcard/|valkey.io} for more details. * * @param key - The key of the sorted set. * @returns The number of elements in the sorted set. @@ -3198,16 +3235,15 @@ export class BaseClient { /** * Returns the cardinality of the intersection of the sorted sets specified by `keys`. * - * See https://valkey.io/commands/zintercard/ for more details. - * + * @see {@link https://valkey.io/commands/zintercard/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param keys - The keys of the sorted sets to intersect. * @param limit - An optional argument that can be used to specify a maximum number for the * intersection cardinality. If limit is not supplied, or if it is set to `0`, there will be no limit. * @returns The cardinality of the intersection of the given sorted sets. * - * since - Redis version 7.0.0. - * * @example * ```typescript * const cardinality = await client.zintercard(["key1", "key2"], 10); @@ -3222,15 +3258,14 @@ export class BaseClient { * Returns the difference between the first sorted set and all the successive sorted sets. * To get the elements with their scores, see {@link zdiffWithScores}. * - * See https://valkey.io/commands/zdiff/ for more details. - * + * @see {@link https://valkey.io/commands/zdiff/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. + * * @param keys - The keys of the sorted sets. * @returns An `array` of elements representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. * - * since Valkey version 6.2.0. - * * @example * ```typescript * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0, "member3": 3.0}); @@ -3248,15 +3283,14 @@ export class BaseClient { * Returns the difference between the first sorted set and all the successive sorted sets, with the associated * scores. * - * See https://valkey.io/commands/zdiff/ for more details. - * + * @see {@link https://valkey.io/commands/zdiff/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. + * * @param keys - The keys of the sorted sets. * @returns A map of elements and their scores representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. * - * since Valkey version 6.2.0. - * * @example * ```typescript * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0, "member3": 3.0}); @@ -3277,15 +3311,14 @@ export class BaseClient { * the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are * treated as empty sets. * - * See https://valkey.io/commands/zdiffstore/ for more details. - * + * @see {@link https://valkey.io/commands/zdiffstore/|valkey.io} for more details. * @remarks When in cluster mode, all keys in `keys` and `destination` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. + * * @param destination - The key for the resulting sorted set. * @param keys - The keys of the sorted sets to compare. * @returns The number of members in the resulting sorted set stored at `destination`. * - * since Valkey version 6.2.0. - * * @example * ```typescript * await client.zadd("zset1", {"member1": 1.0, "member2": 2.0}); @@ -3305,7 +3338,8 @@ export class BaseClient { } /** Returns the score of `member` in the sorted set stored at `key`. - * See https://valkey.io/commands/zscore/ for more details. + * + * @see {@link https://valkey.io/commands/zscore/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param member - The member whose score is to be retrieved. @@ -3341,15 +3375,14 @@ export class BaseClient { /** * Returns the scores associated with the specified `members` in the sorted set stored at `key`. * - * See https://valkey.io/commands/zmscore/ for more details. + * @see {@link https://valkey.io/commands/zmscore/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the sorted set. * @param members - A list of members in the sorted set. * @returns An `array` of scores corresponding to `members`. * If a member does not exist in the sorted set, the corresponding value in the list will be `null`. * - * since Valkey version 6.2.0. - * * @example * ```typescript * const result = await client.zmscore("zset1", ["member1", "non_existent_member", "member2"]); @@ -3364,7 +3397,8 @@ export class BaseClient { } /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. - * See https://valkey.io/commands/zcount/ for more details. + * + * @see {@link https://valkey.io/commands/zcount/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. @@ -3398,8 +3432,9 @@ export class BaseClient { /** Returns the specified range of elements in the sorted set stored at `key`. * ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. * - * See https://valkey.io/commands/zrange/ for more details. - * To get the elements with their scores, see `zrangeWithScores`. + * To get the elements with their scores, see {@link zrangeWithScores}. + * + * @see {@link https://valkey.io/commands/zrange/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -3437,7 +3472,8 @@ export class BaseClient { /** Returns the specified range of elements with their scores in the sorted set stored at `key`. * Similar to ZRANGE but with a WITHSCORE flag. - * See https://valkey.io/commands/zrange/ for more details. + * + * @see {@link https://valkey.io/commands/zrange/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -3484,9 +3520,10 @@ export class BaseClient { * sorted set at `destination`. If `destination` doesn't exist, a new sorted * set is created; if it exists, it's overwritten. * - * See https://valkey.io/commands/zrangestore/ for more details. - * + * @see {@link https://valkey.io/commands/zrangestore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and `source` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. + * * @param destination - The key for the destination sorted set. * @param source - The key of the source sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -3496,8 +3533,6 @@ export class BaseClient { * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @returns The number of elements in the resulting sorted set. * - * since - Redis version 6.2.0. - * * @example * ```typescript * // Example usage of zrangeStore to retrieve and store all members of a sorted set in ascending order. @@ -3531,9 +3566,8 @@ export class BaseClient { * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. * To get the result directly, see `zinter_withscores`. * - * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. - * - * See https://valkey.io/commands/zinterstore/ for more details. + * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for more details. + * @remarks When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: @@ -3566,7 +3600,7 @@ export class BaseClient { /** * Returns a random member from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for more details. * * @param keys - The key of the sorted set. * @returns A string representing a random member from the sorted set. @@ -3591,7 +3625,7 @@ export class BaseClient { /** * Returns random members from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for more details. * * @param keys - The key of the sorted set. * @param count - The number of members to return. @@ -3622,7 +3656,7 @@ export class BaseClient { /** * Returns random members with scores from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for more details. * * @param keys - The key of the sorted set. * @param count - The number of members to return. @@ -3652,7 +3686,8 @@ export class BaseClient { } /** Returns the length of the string value stored at `key`. - * See https://valkey.io/commands/strlen/ for more details. + * + * @see {@link https://valkey.io/commands/strlen/|valkey.io} for more details. * * @param key - The key to check its length. * @returns - The length of the string value stored at key @@ -3678,7 +3713,8 @@ export class BaseClient { } /** Returns the string representation of the type of the value stored at `key`. - * See https://valkey.io/commands/type/ for more details. + * + * @see {@link https://valkey.io/commands/type/|valkey.io} for more details. * * @param key - The `key` to check its data type. * @returns If the `key` exists, the type of the stored value is returned. Otherwise, a "none" string is returned. @@ -3706,7 +3742,8 @@ export class BaseClient { /** Removes and returns the members with the lowest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the lowest scores are removed and returned. * Otherwise, only one member with the lowest score is removed and returned. - * See https://valkey.io/commands/zpopmin for more details. + * + * @see {@link https://valkey.io/commands/zpopmin/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -3741,9 +3778,9 @@ export class BaseClient { * are provided. * `BZPOPMIN` is the blocking variant of {@link zpopmin}. * - * See https://valkey.io/commands/bzpopmin/ for more details. - * + * @see {@link https://valkey.io/commands/bzpopmin/|valkey.io} for more details. * @remarks When in cluster mode, `keys` must map to the same hash slot. + * * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. @@ -3766,7 +3803,8 @@ export class BaseClient { /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. - * See https://valkey.io/commands/zpopmax for more details. + * + * @see {@link https://valkey.io/commands/zpopmax/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -3801,9 +3839,9 @@ export class BaseClient { * are provided. * `BZPOPMAX` is the blocking variant of {@link zpopmax}. * - * See https://valkey.io/commands/zpopmax/ for more details. - * + * @see {@link https://valkey.io/commands/zpopmax/|valkey.io} for more details. * @remarks When in cluster mode, `keys` must map to the same hash slot. + * * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. @@ -3824,7 +3862,8 @@ export class BaseClient { } /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. - * See https://valkey.io/commands/pttl for more details. + * + * @see {@link https://valkey.io/commands/pttl/|valkey.io} for more details. * * @param key - The key to return its timeout. * @returns TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. @@ -3857,7 +3896,8 @@ export class BaseClient { /** Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. * Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. * These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. - * See https://valkey.io/commands/zremrangebyrank/ for more details. + * + * @see {@link https://valkey.io/commands/zremrangebyrank/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param start - The starting point of the range. @@ -3885,7 +3925,7 @@ export class BaseClient { /** * Removes all elements in the sorted set stored at `key` with lexicographical order between `minLex` and `maxLex`. * - * See https://valkey.io/commands/zremrangebylex/ for more details. + * @see {@link https://valkey.io/commands/zremrangebylex/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. @@ -3919,7 +3959,8 @@ export class BaseClient { } /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. - * See https://valkey.io/commands/zremrangebyscore/ for more details. + * + * @see {@link https://valkey.io/commands/zremrangebyscore/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to remove from. Can be positive/negative infinity, or specific score and inclusivity. @@ -3955,7 +3996,7 @@ export class BaseClient { /** * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. * - * See https://valkey.io/commands/zlexcount/ for more details. + * @see {@link https://valkey.io/commands/zlexcount/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. @@ -3985,8 +4026,9 @@ export class BaseClient { } /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. - * See https://valkey.io/commands/zrank for more details. - * To get the rank of `member` with its score, see `zrankWithScore`. + * To get the rank of `member` with its score, see {@link zrankWithScore}. + * + * @see {@link https://valkey.io/commands/zrank/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -4012,15 +4054,15 @@ export class BaseClient { } /** Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. - * See https://valkey.io/commands/zrank for more details. + * + * @see {@link https://valkey.io/commands/zrank/|valkey.io} for more details. + * @remarks Since Valkey version 7.2.0. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. * @returns A list containing the rank and score of `member` in the sorted set. * If `key` doesn't exist, or if `member` is not present in the set, null will be returned. * - * since - Redis version 7.2.0. - * * @example * ```typescript * // Example usage of zrank_withscore method to retrieve the rank and score of a member in a sorted set @@ -4047,7 +4089,7 @@ export class BaseClient { * scores are ordered from the highest to lowest, starting from 0. * To get the rank of `member` with its score, see {@link zrevrankWithScore}. * - * See https://valkey.io/commands/zrevrank/ for more details. + * @see {@link https://valkey.io/commands/zrevrank/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -4068,7 +4110,8 @@ export class BaseClient { * Returns the rank of `member` in the sorted set stored at `key` with its * score, where scores are ordered from the highest to lowest, starting from 0. * - * See https://valkey.io/commands/zrevrank/ for more details. + * @see {@link https://valkey.io/commands/zrevrank/|valkey.io} for more details. + * @remarks Since Valkey version 7.2.0. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -4076,8 +4119,6 @@ export class BaseClient { * are ordered from high to low based on scores. * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. * - * since - Valkey version 7.2.0. - * * @example * ```typescript * const result = await client.zrevankWithScore("my_sorted_set", "member2"); @@ -4093,7 +4134,8 @@ export class BaseClient { /** * Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - * See https://valkey.io/commands/xadd/ for more details. + * + * @see {@link https://valkey.io/commands/xadd/|valkey.io} for more details. * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. @@ -4111,7 +4153,7 @@ export class BaseClient { /** * Removes the specified entries by id from a stream, and returns the number of entries deleted. * - * See https://valkey.io/commands/xdel for more details. + * @see {@link https://valkey.io/commands/xdel/|valkey.io} for more details. * * @param key - The key of the stream. * @param ids - An array of entry ids. @@ -4130,7 +4172,8 @@ export class BaseClient { /** * Trims the stream stored at `key` by evicting older entries. - * See https://valkey.io/commands/xtrim/ for more details. + * + * @see {@link https://valkey.io/commands/xtrim/|valkey.io} for more details. * * @param key - the key of the stream * @param options - options detailing how to trim the stream. @@ -4145,7 +4188,8 @@ export class BaseClient { /** * Reads entries from the given streams. - * See https://valkey.io/commands/xread/ for more details. + * + * @see {@link https://valkey.io/commands/xread/|valkey.io} for more details. * * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. * @param options - options detailing how to read the stream. @@ -4176,7 +4220,7 @@ export class BaseClient { /** * Returns the number of entries in the stream stored at `key`. * - * See https://valkey.io/commands/xlen/ for more details. + * @see {@link https://valkey.io/commands/xlen/|valkey.io} for more details. * * @param key - The key of the stream. * @returns The number of entries in the stream. If `key` does not exist, returns `0`. @@ -4194,7 +4238,7 @@ export class BaseClient { /** * Returns stream message summary information for pending messages matching a given range of IDs. * - * See https://valkey.io/commands/xpending/ for more details. + * @see {@link https://valkey.io/commands/xpending/|valkey.io} for more details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4223,7 +4267,7 @@ export class BaseClient { /** * Returns an extended form of stream message information for pending messages matching a given range of IDs. * - * See https://valkey.io/commands/xpending/ for more details. + * @see {@link https://valkey.io/commands/xpending/|valkey.io} for more details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4266,7 +4310,7 @@ export class BaseClient { * Returns the list of all consumers and their attributes for the given consumer group of the * stream stored at `key`. * - * See https://valkey.io/commands/xinfo-consumers/ for more details. + * @see {@link https://valkey.io/commands/xinfo-consumers/|valkey.io} for more details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4298,7 +4342,7 @@ export class BaseClient { /** * Changes the ownership of a pending message. * - * See https://valkey.io/commands/xclaim/ for more details. + * @see {@link https://valkey.io/commands/xclaim/|valkey.io} for more details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4334,9 +4378,8 @@ export class BaseClient { /** * Transfers ownership of pending stream entries that match the specified criteria. * - * See https://valkey.io/commands/xautoclaim/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/xautoclaim/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4390,9 +4433,8 @@ export class BaseClient { /** * Transfers ownership of pending stream entries that match the specified criteria. * - * See https://valkey.io/commands/xautoclaim/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/xautoclaim/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4453,7 +4495,7 @@ export class BaseClient { * Changes the ownership of a pending message. This function returns an `array` with * only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. * - * See https://valkey.io/commands/xclaim/ for more details. + * @see {@link https://valkey.io/commands/xclaim/|valkey.io} for more details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -4486,7 +4528,7 @@ export class BaseClient { /** * Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-create/ for more details. + * @see {@link https://valkey.io/commands/xgroup-create/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupName - The newly created consumer group name. @@ -4514,7 +4556,7 @@ export class BaseClient { /** * Destroys the consumer group `groupname` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-destroy/ for more details. + * @see {@link https://valkey.io/commands/xgroup-destroy/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupname - The newly created consumer group name. @@ -4536,12 +4578,15 @@ export class BaseClient { /** * Returns information about the stream stored at `key`. * + * @see {@link https://valkey.io/commands/xinfo-stream/|valkey.io} for more details. + * * @param key - The key of the stream. * @param fullOptions - If `true`, returns verbose information with a limit of the first 10 PEL entries. * If `number` is specified, returns verbose information limiting the returned PEL entries. * If `0` is specified, returns verbose information with no limit. * @returns A {@link ReturnTypeXinfoStream} of detailed stream information for the given `key`. See * the example for a sample response. + * * @example * ```typescript * const infoResult = await client.xinfoStream("my_stream"); @@ -4606,7 +4651,7 @@ export class BaseClient { /** * Creates a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-createconsumer for more details. + * @see {@link https://valkey.io/commands/xgroup-createconsumer/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupName - The consumer group name. @@ -4632,7 +4677,7 @@ export class BaseClient { /** * Deletes a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-delconsumer for more details. + * @see {@link https://valkey.io/commands/xgroup-delconsumer/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupName - The consumer group name. @@ -4667,7 +4712,8 @@ export class BaseClient { * 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/lindex/ for more details. + * + * @see {@link https://valkey.io/commands/lindex/|valkey.io} for more details. * * @param key - The `key` of the list. * @param index - The `index` of the element in the list to retrieve. @@ -4695,7 +4741,7 @@ export class BaseClient { /** * Inserts `element` in the list at `key` either before or after the `pivot`. * - * See https://valkey.io/commands/linsert/ for more details. + * @see {@link https://valkey.io/commands/linsert/|valkey.io} for more details. * * @param key - The key of the list. * @param position - The relative position to insert into - either `InsertPosition.Before` or @@ -4725,7 +4771,8 @@ export class BaseClient { /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). - * See https://valkey.io/commands/persist/ for more details. + * + * @see {@link https://valkey.io/commands/persist/|valkey.io} for more details. * * @param key - The key to remove the existing timeout on. * @returns `false` if `key` does not exist or does not have an associated timeout, `true` if the timeout has been removed. @@ -4744,9 +4791,10 @@ export class BaseClient { /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. - * See https://valkey.io/commands/rename/ for more details. * + * @see {@link https://valkey.io/commands/rename/|valkey.io} for more details. * @remarks When in cluster mode, `key` and `newKey` must map to the same hash slot. + * * @param key - The key to rename. * @param newKey - The new name of the key. * @returns - If the `key` was successfully renamed, return "OK". If `key` does not exist, an error is thrown. @@ -4765,9 +4813,10 @@ export class BaseClient { /** * Renames `key` to `newkey` if `newkey` does not yet exist. - * See https://valkey.io/commands/renamenx/ for more details. * + * @see {@link https://valkey.io/commands/renamenx/|valkey.io} for more details. * @remarks When in cluster mode, `key` and `newKey` must map to the same hash slot. + * * @param key - The key to rename. * @param newKey - The new name of the key. * @returns - If the `key` was successfully renamed, returns `true`. Otherwise, returns `false`. @@ -4789,11 +4838,11 @@ export class BaseClient { * Pop an element from the tail of the first list that is non-empty, * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://valkey.io/commands/brpop/ for more details. * - * @remarks - * 1. When in cluster mode, all `keys` must map to the same hash slot. - * 2. `BRPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * @see {@link https://valkey.io/commands/brpop/|valkey.io} for more details. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks `BRPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, @@ -4817,11 +4866,11 @@ export class BaseClient { * Pop an element from the head of the first list that is non-empty, * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://valkey.io/commands/blpop/ for more details. * - * @remarks - * 1. When in cluster mode, all `keys` must map to the same hash slot. - * 2. `BLPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * @see {@link https://valkey.io/commands/blpop/|valkey.io} for more details. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks `BLPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, @@ -4844,7 +4893,7 @@ export class BaseClient { * Creates a new structure if the `key` does not exist. * When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. * - * See https://valkey.io/commands/pfadd/ for more details. + * @see {@link https://valkey.io/commands/pfadd/|valkey.io} for more details. * * @param key - The key of the HyperLogLog data structure to add elements into. * @param elements - An array of members to add to the HyperLogLog stored at `key`. @@ -4865,9 +4914,9 @@ export class BaseClient { /** Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. * - * See https://valkey.io/commands/pfcount/ for more details. - * + * @see {@link https://valkey.io/commands/pfcount/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * * @param keys - The keys of the HyperLogLog data structures to be analyzed. * @returns - The approximated cardinality of given HyperLogLog data structures. * The cardinality of a key that does not exist is `0`. @@ -4885,9 +4934,9 @@ export class BaseClient { * Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is * treated as one of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. * - * See https://valkey.io/commands/pfmerge/ for more details. - * + * @see {@link https://valkey.io/commands/pfmerge/|valkey.io} for more details. * @remarks When in Cluster mode, all keys in `sourceKeys` and `destination` must map to the same hash slot. + * * @param destination - The key of the destination HyperLogLog where the merged data sets will be stored. * @param sourceKeys - The keys of the HyperLogLog structures to be merged. * @returns A simple "OK" response. @@ -4909,9 +4958,9 @@ export class BaseClient { return this.createWritePromise(createPfMerge(destination, sourceKeys)); } - /** Returns the internal encoding for the Redis object stored at `key`. + /** Returns the internal encoding for the Valkey object stored at `key`. * - * See https://valkey.io/commands/object-encoding for more details. + * @see {@link https://valkey.io/commands/object-encoding/|valkey.io} for more details. * * @param key - The `key` of the object to get the internal encoding of. * @returns - If `key` exists, returns the internal encoding of the object stored at `key` as a string. @@ -4926,9 +4975,9 @@ export class BaseClient { return this.createWritePromise(createObjectEncoding(key)); } - /** Returns the logarithmic access frequency counter of a Redis object stored at `key`. + /** Returns the logarithmic access frequency counter of a Valkey object stored at `key`. * - * See https://valkey.io/commands/object-freq for more details. + * @see {@link https://valkey.io/commands/object-freq/|valkey.io} for more details. * * @param key - The `key` of the object to get the logarithmic access frequency counter of. * @returns - If `key` exists, returns the logarithmic access frequency counter of the object @@ -4946,7 +4995,7 @@ export class BaseClient { /** * Returns the time in seconds since the last access to the value stored at `key`. * - * See https://valkey.io/commands/object-idletime/ for more details. + * @see {@link https://valkey.io/commands/object-idletime/|valkey.io} for more details. * * @param key - The key of the object to get the idle time of. * @returns If `key` exists, returns the idle time in seconds. Otherwise, returns `null`. @@ -4964,7 +5013,7 @@ export class BaseClient { /** * Returns the reference count of the object stored at `key`. * - * See https://valkey.io/commands/object-refcount/ for more details. + * @see {@link https://valkey.io/commands/object-refcount/|valkey.io} for more details. * * @param key - The `key` of the object to get the reference count of. * @returns If `key` exists, returns the reference count of the object stored at `key` as a `number`. @@ -4983,11 +5032,10 @@ export class BaseClient { /** * Invokes a previously loaded function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. - * + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. @@ -5011,11 +5059,10 @@ export class BaseClient { /** * Invokes a previously loaded read-only function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. - * + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. @@ -5042,7 +5089,8 @@ export class BaseClient { * 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. + * @see {@link https://valkey.io/commands/lpos/|valkey.io} for more details. + * @remarks Since Valkey version 6.0.6. * * @param key - The name of the list. * @param element - The value to search for within the list. @@ -5050,8 +5098,6 @@ export class BaseClient { * @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"]); @@ -5071,7 +5117,7 @@ export class BaseClient { * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can * optionally be provided to count the number of bits in a specific string interval. * - * See https://valkey.io/commands/bitcount for more details. + * @see {@link https://valkey.io/commands/bitcount/|valkey.io} for more details. * * @param key - The key for the string to count the set bits of. * @param options - The offset options. @@ -5098,7 +5144,7 @@ export class BaseClient { * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. * - * See https://valkey.io/commands/geoadd/ for more details. + * @see {@link https://valkey.io/commands/geoadd/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param membersToGeospatialData - A mapping of member names to their corresponding positions - see @@ -5132,9 +5178,8 @@ export class BaseClient { * Returns the members of a sorted set populated with geospatial information using {@link geoadd}, * which are within the borders of the area specified by a given shape. * - * See https://valkey.io/commands/geosearch/ for more details. - * - * since - Valkey 6.2.0 and above. + * @see {@link https://valkey.io/commands/geosearch/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the sorted set. * @param searchFrom - The query's center point options, could be one of: @@ -5214,11 +5259,9 @@ export class BaseClient { * * To get the result directly, see {@link geosearch}. * - * See https://valkey.io/commands/geosearchstore/ for more details. - * - * since - Valkey 6.2.0 and above. - * + * @see {@link https://valkey.io/commands/geosearchstore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and `source` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. * * @param destination - The key of the destination sorted set. * @param source - The key of the sorted set. @@ -5289,7 +5332,7 @@ export class BaseClient { * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. * - * See https://valkey.io/commands/geopos for more details. + * @see {@link https://valkey.io/commands/geopos/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param members - The members for which to get the positions. @@ -5318,9 +5361,10 @@ export class BaseClient { * Pops a member-score pair from the first non-empty sorted set, with the given `keys` * being checked in the order they are provided. * - * See https://valkey.io/commands/zmpop/ for more details. - * + * @see {@link https://valkey.io/commands/zmpop/|valkey.io} for more details. * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. @@ -5329,8 +5373,6 @@ export class BaseClient { * was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. * - * since Valkey version 7.0.0. - * * @example * ```typescript * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); @@ -5352,12 +5394,11 @@ export class BaseClient { * checked in the order they are provided. Blocks the connection when there are no members * to pop from any of the given sorted sets. `BZMPOP` is the blocking variant of {@link zmpop}. * - * See https://valkey.io/commands/bzmpop/ for more details. + * @see {@link https://valkey.io/commands/bzmpop/|valkey.io} for more details. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @remarks `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | Valkey Glide Wiki} for more details and best practices. + * @remarks Since Valkey version 7.0.0. * - * @remarks - * 1. When in cluster mode, all `keys` must map to the same hash slot. - * 2. `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | the wiki} - * for more details and best practices. * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. @@ -5368,8 +5409,6 @@ export class BaseClient { * was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. * - * since Valkey version 7.0.0. - * * @example * ```typescript * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); @@ -5394,7 +5433,7 @@ export class BaseClient { * If `member` does not exist in the sorted set, it is added with `increment` as its score. * If `key` does not exist, a new sorted set is created with the specified member as its sole member. * - * See https://valkey.io/commands/zincrby/ for details. + * @see {@link https://valkey.io/commands/zincrby/|valkey.io} for details. * * @param key - The key of the sorted set. * @param increment - The score increment. @@ -5425,7 +5464,7 @@ export class BaseClient { /** * Iterates incrementally over a sorted set. * - * See https://valkey.io/commands/zscan for more details. + * @see {@link https://valkey.io/commands/zscan/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of @@ -5472,7 +5511,7 @@ export class BaseClient { /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * - * See https://valkey.io/commands/geodist/ for more details. + * @see {@link https://valkey.io/commands/geodist/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param member1 - The name of the first member. @@ -5501,7 +5540,7 @@ export class BaseClient { /** * Returns the `GeoHash` strings representing the positions of all the specified `members` in the sorted set stored at `key`. * - * See https://valkey.io/commands/geohash/ for more details. + * @see {@link https://valkey.io/commands/geohash/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param members - The array of members whose `GeoHash` strings are to be retrieved. @@ -5528,11 +5567,9 @@ export class BaseClient { /** * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`. * - * since Valkey version 7.0.0. - * + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for more details. * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. - * - * See https://valkey.io/commands/lcs/ for more details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -5553,11 +5590,9 @@ export class BaseClient { /** * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`. * - * since Valkey version 7.0.0. - * + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for more details. * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. - * - * See https://valkey.io/commands/lcs/ for more details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -5578,11 +5613,9 @@ export class BaseClient { * Returns the indices and lengths of the longest common subsequences between strings stored at * `key1` and `key2`. * - * since Valkey version 7.0.0. - * + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for more details. * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot. - * - * See https://valkey.io/commands/lcs/ for more details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -5633,9 +5666,9 @@ export class BaseClient { /** * Updates the last access time of the specified keys. * - * See https://valkey.io/commands/touch/ for more details. - * + * @see {@link https://valkey.io/commands/touch/|valkey.io} for more details. * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + * * @param keys - The keys to update the last access time of. * @returns The number of keys that were updated. A key is ignored if it doesn't exist. * @@ -5656,9 +5689,9 @@ export class BaseClient { * will only execute commands if the watched keys are not modified before execution of the * transaction. Executing a transaction will automatically flush all previously watched keys. * - * See https://valkey.io/commands/watch/ and https://valkey.io/topics/transactions/#cas for more details. - * + * @see {@link https://valkey.io/commands/watch/|valkey.io} and {@link https://valkey.io/topics/transactions/#cas|Valkey Glide Wiki} for more details. * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + * * @param keys - The keys to watch. * @returns A simple "OK" response. * @@ -5688,7 +5721,7 @@ export class BaseClient { * acknowledged by at least `numreplicas` of replicas. If `timeout` is reached, the command returns * the number of replicas that were not yet reached. * - * See https://valkey.io/commands/wait/ for more details. + * @see {@link https://valkey.io/commands/wait/|valkey.io} for more details. * * @param numreplicas - The number of replicas to reach. * @param timeout - The timeout value specified in milliseconds. A value of 0 will block indefinitely. @@ -5710,7 +5743,7 @@ export class BaseClient { * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, * the string is padded with zero bytes to make `offset` fit. Creates the `key` if it doesn't exist. * - * See https://valkey.io/commands/setrange/ for more details. + * @see {@link https://valkey.io/commands/setrange/|valkey.io} for more details. * * @param key - The key of the string to update. * @param offset - The position in the string where `value` should be written. @@ -5737,7 +5770,7 @@ export class BaseClient { * Appends a `value` to a `key`. If `key` does not exist it is created and set as an empty string, * so `APPEND` will be similar to {@link set} in this special case. * - * See https://valkey.io/commands/append/ for more details. + * @see {@link https://valkey.io/commands/append/|valkey.io} for more details. * * @param key - The key of the string. * @param value - The key of the string. @@ -5762,16 +5795,15 @@ export class BaseClient { /** * Pops one or more elements from the first non-empty list from the provided `keys`. * - * See https://valkey.io/commands/lmpop/ for more details. - * + * @see {@link https://valkey.io/commands/lmpop/|valkey.io} for more details. * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param keys - An array of keys to lists. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. * @param count - (Optional) The maximum number of popped elements. * @returns A `Record` of key-name mapped array of popped elements. * - * since Valkey version 7.0.0. - * * @example * ```typescript * await client.lpush("testKey", ["one", "two", "three"]); @@ -5792,9 +5824,10 @@ export class BaseClient { * Blocks the connection until it pops one or more elements from the first non-empty list from the * provided `key`. `BLMPOP` is the blocking variant of {@link lmpop}. * - * See https://valkey.io/commands/blmpop/ for more details. - * + * @see {@link https://valkey.io/commands/blmpop/|valkey.io} for more details. * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @remarks Since Valkey version 7.0.0. + * * @param keys - An array of keys to lists. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. @@ -5802,8 +5835,6 @@ export class BaseClient { * @returns - A `Record` of `key` name mapped array of popped elements. * If no member could be popped and the timeout expired, returns `null`. * - * since Valkey version 7.0.0. - * * @example * ```typescript * await client.lpush("testKey", ["one", "two", "three"]); @@ -5827,7 +5858,7 @@ export class BaseClient { * Lists the currently active channels. * The command is routed to all nodes, and aggregates the response to a single array. * - * See https://valkey.io/commands/pubsub-channels for more details. + * @see {@link https://valkey.io/commands/pubsub-channels/|valkey.io} for more details. * * @param pattern - A glob-style pattern to match active channels. * If not provided, all active channels are returned. @@ -5854,7 +5885,7 @@ export class BaseClient { * not the count of clients subscribed to patterns. * The command is routed to all nodes, and aggregates the response to the sum of all pattern subscriptions. * - * See https://valkey.io/commands/pubsub-numpat for more details. + * @see {@link https://valkey.io/commands/pubsub-numpat/|valkey.io} for more details. * * @returns The number of unique patterns. * @@ -5874,7 +5905,7 @@ export class BaseClient { * Note that it is valid to call this command without channels. In this case, it will just return an empty map. * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. * - * See https://valkey.io/commands/pubsub-numsub for more details. + * @see {@link https://valkey.io/commands/pubsub-numsub/|valkey.io} for more details. * * @param channels - The list of channels to query for the number of subscribers. * If not provided, returns an empty map. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index ce34cbec70..e63f7f8fb1 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -58,7 +58,7 @@ import { Transaction } from "./Transaction"; export namespace GlideClientConfiguration { /** * Enum representing pubsub subscription modes. - * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + * @see {@link https://valkey.io/docs/topics/pubsub/|Valkey PubSub Documentation} for more details. */ export enum PubSubChannelModes { /** @@ -131,8 +131,8 @@ export type GlideClientConfiguration = BaseClientConfiguration & { /** * Client used for connection to standalone Redis servers. - * For full documentation, see - * https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#standalone + * + * @see For full documentation refer to {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#standalone|Valkey Glide Wiki}. */ export class GlideClient extends BaseClient { /** @@ -169,8 +169,10 @@ export class GlideClient extends BaseClient { ); } - /** Execute a transaction by processing the queued commands. - * See https://redis.io/topics/Transactions/ for details on Redis Transactions. + /** + * Execute a transaction by processing the queued commands. + * + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#transaction|Valkey Glide Wiki} for details on Valkey Transactions. * * @param transaction - A Transaction object containing a list of commands to be executed. * @param decoder - (Optional) {@link Decoder} type which defines how to handle the responses. If not set, the default decoder from the client config will be used. @@ -199,8 +201,7 @@ export class GlideClient extends BaseClient { * * Note: An error will occur if the string decoder is used with commands that return only bytes as a response. * - * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) - * for details on the restrictions and limitations of the custom command API. + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command|Valkey Glide Wiki} for details on the restrictions and limitations of the custom command API. * * @example * ```typescript @@ -219,7 +220,7 @@ export class GlideClient extends BaseClient { } /** Ping the Redis server. - * See https://valkey.io/commands/ping/ for details. + * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -251,7 +252,7 @@ export class GlideClient extends BaseClient { } /** Get information and statistics about the Redis server. - * See https://valkey.io/commands/info/ for details. + * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -262,7 +263,7 @@ export class GlideClient extends BaseClient { } /** Change the currently selected Redis database. - * See https://valkey.io/commands/select/ for details. + * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. * @returns A simple OK response. @@ -279,7 +280,7 @@ export class GlideClient extends BaseClient { } /** Get the name of the primary's connection. - * See https://valkey.io/commands/client-getname/ for more details. + * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for more details. * * @returns the name of the client connection as a string if a name is set, or null if no name is assigned. * @@ -295,7 +296,7 @@ export class GlideClient extends BaseClient { } /** Rewrite the configuration file with the current configuration. - * See https://valkey.io/commands/config-rewrite/ for details. + * @see {@link https://valkey.io/commands/config-rewrite/|valkey.io} for details. * * @returns "OK" when the configuration was rewritten properly. Otherwise, an error is thrown. * @@ -311,7 +312,8 @@ export class GlideClient extends BaseClient { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://valkey.io/commands/config-resetstat/ for details. + * + * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * * @returns always "OK". * @@ -327,7 +329,7 @@ export class GlideClient extends BaseClient { } /** Returns the current connection id. - * See https://valkey.io/commands/client-id/ for details. + * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * * @returns the id of the client. */ @@ -336,7 +338,8 @@ export class GlideClient extends BaseClient { } /** Reads the configuration parameters of a running Redis server. - * See https://valkey.io/commands/config-get/ for details. + * + * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @@ -355,11 +358,11 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createConfigGet(parameters)); } - /** Set configuration parameters to the specified values. - * See https://valkey.io/commands/config-set/ for details. + /** + * Set configuration parameters to the specified values. * + * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. - * * @returns "OK" when the configuration was set properly. Otherwise an error is thrown. * * @example @@ -374,7 +377,7 @@ export class GlideClient extends BaseClient { } /** Echoes the provided `message` back. - * See https://valkey.io/commands/echo for more details. + * @see {@link https://valkey.io/commands/echo|valkey.io} for more details. * * @param message - The message to be echoed back. * @returns The provided `message`. @@ -391,7 +394,7 @@ export class GlideClient extends BaseClient { } /** Returns the server time - * See https://valkey.io/commands/time/ for details. + * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * * @returns - The current server time as a two items `array`: * A Unix timestamp and the amount of microseconds already elapsed in the current second. @@ -414,7 +417,8 @@ export class GlideClient extends BaseClient { * When `replace` is true, removes the `destination` key first if it already exists, otherwise performs * no action. * - * See https://valkey.io/commands/copy/ for more details. + * @see {@link https://valkey.io/commands/copy/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source value. * @param destination - The key where the value should be copied to. @@ -424,8 +428,6 @@ export class GlideClient extends BaseClient { * value to it. If not provided, no action will be performed if the key already exists. * @returns `true` if `source` was copied, `false` if the `source` was not copied. * - * since Valkey version 6.2.0. - * * @example * ```typescript * const result = await client.copy("set1", "set2"); @@ -453,7 +455,7 @@ export class GlideClient extends BaseClient { /** * Move `key` from the currently selected database to the database specified by `dbIndex`. * - * See https://valkey.io/commands/move/ for more details. + * @see {@link https://valkey.io/commands/move/|valkey.io} for more details. * * @param key - The key to move. * @param dbIndex - The index of the database to move `key` to. @@ -473,7 +475,7 @@ export class GlideClient extends BaseClient { /** * Displays a piece of generative computer art and the server version. * - * See https://valkey.io/commands/lolwut/ for more details. + * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for more details. * * @param options - The LOLWUT options * @returns A piece of generative computer art along with the current server version. @@ -491,9 +493,8 @@ export class GlideClient extends BaseClient { /** * Deletes a library and all its functions. * - * See https://valkey.io/commands/function-delete/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-delete/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The library name to delete. * @returns A simple OK response. @@ -511,9 +512,8 @@ export class GlideClient extends BaseClient { /** * Loads a library to Valkey. * - * See https://valkey.io/commands/function-load/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-load/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. * @param replace - Whether the given library should overwrite a library with the same name if it @@ -539,9 +539,8 @@ export class GlideClient extends BaseClient { /** * Deletes all function libraries. * - * See https://valkey.io/commands/function-flush/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @returns A simple OK response. @@ -559,9 +558,8 @@ export class GlideClient extends BaseClient { /** * Returns information about the functions and libraries. * - * See https://valkey.io/commands/function-list/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param options - Parameters to filter and request additional info. * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. @@ -595,15 +593,13 @@ export class GlideClient extends BaseClient { * Returns information about the function that's currently running and information about the * available execution engines. * - * See https://valkey.io/commands/function-stats/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @returns A `Record` with two keys: * - `"running_script"` with information about the running script. * - `"engines"` with information about available engines and their stats. - * - * See example for more details. + * - see example for more details. * * @example * ```typescript @@ -664,7 +660,7 @@ export class GlideClient extends BaseClient { /** * Deletes all the keys of all the existing databases. This command never fails. * - * See https://valkey.io/commands/flushall/ for more details. + * @see {@link https://valkey.io/commands/flushall/|valkey.io} for more details. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @returns `OK`. @@ -682,7 +678,7 @@ export class GlideClient extends BaseClient { /** * Deletes all the keys of the currently selected database. This command never fails. * - * See https://valkey.io/commands/flushdb/ for more details. + * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for more details. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @returns `OK`. @@ -700,7 +696,7 @@ export class GlideClient extends BaseClient { /** * Returns the number of keys in the currently selected database. * - * See https://valkey.io/commands/dbsize/ for more details. + * @see {@link https://valkey.io/commands/dbsize/|valkey.io} for more details. * * @returns The number of keys in the currently selected database. * @@ -716,7 +712,7 @@ export class GlideClient extends BaseClient { /** Publish a message on pubsub channel. * - * See https://valkey.io/commands/publish for more details. + * @see {@link https://valkey.io/commands/publish/|valkey.io} for more details. * * @param message - Message to publish. * @param channel - Channel to publish the message on. @@ -742,7 +738,7 @@ export class GlideClient extends BaseClient { * * To store the result into a new key, see {@link sortStore}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - The {@link SortOptions}. @@ -772,7 +768,8 @@ export class GlideClient extends BaseClient { * * This command is routed depending on the client's {@link ReadFrom} strategy. * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. + * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - The {@link SortOptions}. @@ -803,9 +800,9 @@ export class GlideClient extends BaseClient { * * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. * - * See https://valkey.io/commands/sort for more details. - * + * @see {@link https://valkey.io/commands/sort|valkey.io} for more details. * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * * @param key - The key of the list, set, or sorted set to be sorted. * @param destination - The key where the sorted result will be stored. * @param options - The {@link SortOptions}. @@ -833,7 +830,7 @@ export class GlideClient extends BaseClient { * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. * - * See https://valkey.io/commands/lastsave/ for more details. + * @see {@link https://valkey.io/commands/lastsave/|valkey.io} for more details. * * @returns `UNIX TIME` of the last DB save executed with success. * @example @@ -849,7 +846,7 @@ export class GlideClient extends BaseClient { /** * Returns a random existing key name from the currently selected database. * - * See https://valkey.io/commands/randomkey/ for more details. + * @see {@link https://valkey.io/commands/randomkey/|valkey.io} for more details. * * @returns A random existing key name from the currently selected database. * @@ -867,7 +864,7 @@ export class GlideClient extends BaseClient { * Flushes all the previously watched keys for a transaction. Executing a transaction will * automatically flush all previously watched keys. * - * See https://valkey.io/commands/unwatch/ and https://valkey.io/topics/transactions/#cas for more details. + * @see {@link https://valkey.io/commands/unwatch/|valkey.io} and {@link https://valkey.io/topics/transactions/#cas|Valkey Glide Wiki} for more details. * * @returns A simple "OK" response. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 84f875355b..0c3698e170 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -88,7 +88,7 @@ export type PeriodicChecks = export namespace GlideClusterClientConfiguration { /** * Enum representing pubsub subscription modes. - * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + * @see {@link https://valkey.io/docs/topics/pubsub/|Valkey PubSub Documentation} for more details. */ export enum PubSubChannelModes { /** @@ -285,8 +285,8 @@ function toProtobufRoute( /** * Client used for connection to cluster Redis servers. - * For full documentation, see - * https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#cluster + * + * @see For full documentation refer to {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#cluster|Valkey Glide Wiki}. */ export class GlideClusterClient extends BaseClient { /** @@ -346,8 +346,7 @@ export class GlideClusterClient extends BaseClient { * * Note: An error will occur if the string decoder is used with commands that return only bytes as a response. * - * See the [Glide for Valkey Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) - * for details on the restrictions and limitations of the custom command API. + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command|Glide for Valkey Wiki} for details on the restrictions and limitations of the custom command API. * * @example * ```typescript @@ -367,8 +366,9 @@ export class GlideClusterClient extends BaseClient { }); } - /** Execute a transaction by processing the queued commands. - * See https://redis.io/topics/Transactions/ for details on Redis Transactions. + /** + * Execute a transaction by processing the queued commands. + * @see {@link https://redis.io/topics/Transactions/|Valkey Glide Wiki} for details on Redis Transactions. * * @param transaction - A ClusterTransaction object containing a list of commands to be executed. * @param route - If `route` is not provided, the transaction will be routed to the slot owner of the first key found in the transaction. @@ -401,7 +401,8 @@ export class GlideClusterClient extends BaseClient { } /** Ping the Redis server. - * See https://valkey.io/commands/ping/ for details. + * + * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -437,7 +438,7 @@ export class GlideClusterClient extends BaseClient { } /** Get information and statistics about the Redis server. - * See https://valkey.io/commands/info/ for details. + * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -457,7 +458,7 @@ export class GlideClusterClient extends BaseClient { } /** Get the name of the connection to which the request is routed. - * See https://valkey.io/commands/client-getname/ for more details. + * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for details. * * @param route - The command will be routed a random node, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -490,11 +491,10 @@ export class GlideClusterClient extends BaseClient { } /** Rewrite the configuration file with the current configuration. - * See https://valkey.io/commands/config-rewrite/ for details. + * @see {@link https://valkey.io/commands/config-rewrite/|valkey.io} for details. * * @param route - The command will be routed to all nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. - * * @returns "OK" when the configuration was rewritten properly. Otherwise, an error is thrown. * * @example @@ -511,11 +511,10 @@ export class GlideClusterClient extends BaseClient { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://valkey.io/commands/config-resetstat/ for details. + * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * * @param route - The command will be routed to all nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. - * * @returns always "OK". * * @example @@ -532,7 +531,7 @@ export class GlideClusterClient extends BaseClient { } /** Returns the current connection id. - * See https://valkey.io/commands/client-id/ for details. + * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * * @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`. @@ -547,7 +546,7 @@ export class GlideClusterClient extends BaseClient { } /** Reads the configuration parameters of a running Redis server. - * See https://valkey.io/commands/config-get/ for details. + * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @param route - The command will be routed to a random node, unless `route` is provided, in which @@ -582,13 +581,12 @@ export class GlideClusterClient extends BaseClient { } /** Set configuration parameters to the specified values. - * See https://valkey.io/commands/config-set/ for details. + * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. * @param route - The command will be routed to all nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. * If `route` is not provided, the command will be sent to the all nodes. - * * @returns "OK" when the configuration was set properly. Otherwise an error is thrown. * * @example @@ -608,7 +606,7 @@ export class GlideClusterClient extends BaseClient { } /** Echoes the provided `message` back. - * See https://valkey.io/commands/echo for more details. + * @see {@link https://valkey.io/commands/echo/|valkey.io} for details. * * @param message - The message to be echoed back. * @param route - The command will be routed to a random node, unless `route` is provided, in which @@ -639,7 +637,7 @@ export class GlideClusterClient extends BaseClient { } /** Returns the server time. - * See https://valkey.io/commands/time/ for details. + * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * * @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`. @@ -676,17 +674,16 @@ export class GlideClusterClient extends BaseClient { * Copies the value stored at the `source` to the `destination` key. When `replace` is `true`, * removes the `destination` key first if it already exists, otherwise performs no action. * - * See https://valkey.io/commands/copy/ for more details. - * + * @see {@link https://valkey.io/commands/copy/|valkey.io} for details. * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. + * @remarks Since Valkey version 6.2.0. + * * @param source - The key to the source value. * @param destination - The key where the value should be copied to. * @param replace - (Optional) If `true`, the `destination` key should be removed before copying the * value to it. If not provided, no action will be performed if the key already exists. * @returns `true` if `source` was copied, `false` if the `source` was not copied. * - * since Valkey version 6.2.0. - * * @example * ```typescript * const result = await client.copy("set1", "set2", true); @@ -706,7 +703,7 @@ export class GlideClusterClient extends BaseClient { /** * Displays a piece of generative computer art and the server version. * - * See https://valkey.io/commands/lolwut/ for more details. + * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for details. * * @param options - The LOLWUT options. * @param route - The command will be routed to a random node, unless `route` is provided, in which @@ -732,9 +729,8 @@ export class GlideClusterClient extends BaseClient { /** * Invokes a previously loaded function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. @@ -761,9 +757,8 @@ export class GlideClusterClient extends BaseClient { /** * Invokes a previously loaded read-only function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. @@ -791,9 +786,8 @@ export class GlideClusterClient extends BaseClient { /** * Deletes a library and all its functions. * - * See https://valkey.io/commands/function-delete/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-delete/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The library name to delete. * @param route - The command will be routed to all primary node, unless `route` is provided, in which @@ -818,9 +812,8 @@ export class GlideClusterClient extends BaseClient { /** * Loads a library to Valkey. * - * See https://valkey.io/commands/function-load/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-load/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. * @param replace - Whether the given library should overwrite a library with the same name if it @@ -850,9 +843,8 @@ export class GlideClusterClient extends BaseClient { /** * Deletes all function libraries. * - * See https://valkey.io/commands/function-flush/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which @@ -877,9 +869,8 @@ export class GlideClusterClient extends BaseClient { /** * Returns information about the functions and libraries. * - * See https://valkey.io/commands/function-list/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param options - Parameters to filter and request additional info. * @param route - The client will route the command to the nodes defined by `route`. @@ -918,17 +909,15 @@ export class GlideClusterClient extends BaseClient { * Returns information about the function that's currently running and information about the * available execution engines. * - * See https://valkey.io/commands/function-stats/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param route - The client will route the command to the nodes defined by `route`. * If not defined, the command will be routed to all primary nodes. * @returns A `Record` with two keys: * - `"running_script"` with information about the running script. * - `"engines"` with information about available engines and their stats. - * - * See example for more details. + * - See example for more details. * * @example * ```typescript @@ -976,13 +965,13 @@ export class GlideClusterClient extends BaseClient { * Kills a function that is currently executing. * `FUNCTION KILL` terminates read-only functions only. * - * See https://valkey.io/commands/function-kill/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-kill/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param route - (Optional) The client will route the command to the nodes defined by `route`. * If not defined, the command will be routed to all primary nodes. * @returns `OK` if function is terminated. Otherwise, throws an error. + * * @example * ```typescript * await client.functionKill(); @@ -997,7 +986,7 @@ export class GlideClusterClient extends BaseClient { /** * Deletes all the keys of all the existing databases. This command never fails. * - * See https://valkey.io/commands/flushall/ for more details. + * @see {@link https://valkey.io/commands/flushall/|valkey.io} for 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 primary nodes, unless `route` is provided, in which @@ -1019,7 +1008,7 @@ export class GlideClusterClient extends BaseClient { /** * Deletes all the keys of the currently selected database. This command never fails. * - * See https://valkey.io/commands/flushdb/ for more details. + * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for 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 primary nodes, unless `route` is provided, in which @@ -1041,7 +1030,7 @@ export class GlideClusterClient extends BaseClient { /** * Returns the number of keys in the database. * - * See https://valkey.io/commands/dbsize/ for more details. + * @see {@link https://valkey.io/commands/dbsize/|valkey.io} for details. * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -1065,7 +1054,7 @@ export class GlideClusterClient extends BaseClient { * 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. + * @see {@link https://valkey.io/commands/publish} and {@link https://valkey.io/commands/spublish} for more details. * * @param message - Message to publish. * @param channel - Channel to publish the message on. @@ -1100,7 +1089,7 @@ export class GlideClusterClient extends BaseClient { * Lists the currently active shard channels. * The command is routed to all nodes, and aggregates the response to a single array. * - * See https://valkey.io/commands/pubsub-shardchannels for more details. + * @see {@link https://valkey.io/commands/pubsub-shardchannels/|valkey.io} for details. * * @param pattern - A glob-style pattern to match active shard channels. * If not provided, all active shard channels are returned. @@ -1126,7 +1115,7 @@ export class GlideClusterClient extends BaseClient { * Note that it is valid to call this command without channels. In this case, it will just return an empty map. * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. * - * See https://valkey.io/commands/pubsub-shardnumsub for more details. + * @see {@link https://valkey.io/commands/pubsub-shardnumsub/|valkey.io} for details. * * @param channels - The list of shard channels to query for the number of subscribers. * If not provided, returns an empty map. @@ -1155,7 +1144,7 @@ export class GlideClusterClient extends BaseClient { * * To store the result into a new key, see {@link sortStore}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for details. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortClusterOptions}. @@ -1183,7 +1172,7 @@ export class GlideClusterClient extends BaseClient { * * This command is routed depending on the client's {@link ReadFrom} strategy. * - * since Valkey version 7.0.0. + * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortClusterOptions}. @@ -1212,9 +1201,9 @@ export class GlideClusterClient extends BaseClient { * * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. * - * See https://valkey.io/commands/sort for more details. - * + * @see {@link https://valkey.io/commands/sort/|valkey.io} for details. * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * * @param key - The key of the list, set, or sorted set to be sorted. * @param destination - The key where the sorted result will be stored. * @param options - (Optional) {@link SortClusterOptions}. @@ -1240,7 +1229,7 @@ export class GlideClusterClient extends BaseClient { * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. * - * See https://valkey.io/commands/lastsave/ for more details. + * @see {@link https://valkey.io/commands/lastsave/|valkey.io} for details. * * @param route - (Optional) 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`. @@ -1260,7 +1249,7 @@ export class GlideClusterClient extends BaseClient { /** * Returns a random existing key name. * - * See https://valkey.io/commands/randomkey/ for more details. + * @see {@link https://valkey.io/commands/randomkey/|valkey.io} for details. * * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, * in which case the client will route the command to the nodes defined by `route`. @@ -1282,7 +1271,7 @@ export class GlideClusterClient extends BaseClient { * Flushes all the previously watched keys for a transaction. Executing a transaction will * automatically flush all previously watched keys. * - * See https://valkey.io/commands/unwatch/ and https://valkey.io/topics/transactions/#cas for more details. + * @see {@link https://valkey.io/commands/unwatch/|valkey.io} and {@link https://valkey.io/topics/transactions/#cas|Valkey Glide Wiki} for more details. * * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, * in which case the client will route the command to the nodes defined by `route`. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7ac86a8f48..b24d4f8973 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -296,8 +296,7 @@ export class BaseTransaction> { } /** Get the value associated with the given key, or null if no such value exists. - * - * See https://valkey.io/commands/get/ for details. + * @see {@link https://valkey.io/commands/get/|valkey.io} for details. * * @param key - The key to retrieve from the database. * @@ -309,15 +308,15 @@ export class BaseTransaction> { /** * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. - * See https://valkey.io/commands/getex for more details. + * + * @see {@link https://valkey.io/commands/getex/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key to retrieve from the database. * @param options - (Optional) set expiriation to the given key. * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. * - * since - Valkey 6.2.0 and above. - * * Command Response - If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. */ public getex( @@ -330,7 +329,7 @@ export class BaseTransaction> { /** * Gets a string value associated with the given `key`and deletes the key. * - * See https://valkey.io/commands/getdel/ for details. + * @see {@link https://valkey.io/commands/getdel/|valkey.io} for details. * * @param key - The key to retrieve from the database. * @@ -347,7 +346,7 @@ export class BaseTransaction> { * penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` * or `end` are out of range, returns the substring within the valid range of the string. * - * See https://valkey.io/commands/getrange/ for details. + * @see {@link https://valkey.io/commands/getrange/|valkey.io} for details. * * @param key - The key of the string. * @param start - The starting offset. @@ -360,7 +359,7 @@ export class BaseTransaction> { } /** Set the given key with the given value. Return value is dependent on the passed options. - * See https://valkey.io/commands/set/ for details. + * @see {@link https://valkey.io/commands/set/|valkey.io} for details. * * @param key - The key to store. * @param value - The value to store with the given key. @@ -375,7 +374,7 @@ export class BaseTransaction> { } /** Ping the Redis server. - * See https://valkey.io/commands/ping/ for details. + * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -388,7 +387,7 @@ export class BaseTransaction> { } /** Get information and statistics about the Redis server. - * See https://valkey.io/commands/info/ for details. + * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -400,7 +399,7 @@ export class BaseTransaction> { } /** Remove the specified keys. A key is ignored if it does not exist. - * See https://valkey.io/commands/del/ for details. + * @see {@link https://valkey.io/commands/del/|valkey.io} for details. * * @param keys - A list of keys to be deleted from the database. * @@ -411,7 +410,7 @@ export class BaseTransaction> { } /** Get the name of the connection on which the transaction is being executed. - * See https://valkey.io/commands/client-getname/ for more details. + * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for details. * * Command Response - the name of the client connection as a string if a name is set, or null if no name is assigned. */ @@ -420,7 +419,7 @@ export class BaseTransaction> { } /** Rewrite the configuration file with the current configuration. - * See https://valkey.io/commands/select/ for details. + * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * Command Response - "OK" when the configuration was rewritten properly. Otherwise, the transaction fails with an error. */ @@ -429,7 +428,7 @@ export class BaseTransaction> { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://valkey.io/commands/config-resetstat/ for details. + * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * * Command Response - always "OK". */ @@ -438,7 +437,7 @@ export class BaseTransaction> { } /** Retrieve the values of multiple keys. - * See https://valkey.io/commands/mget/ for details. + * @see {@link https://valkey.io/commands/mget/|valkey.io} for details. * * @param keys - A list of keys to retrieve values for. * @@ -450,7 +449,7 @@ export class BaseTransaction> { } /** Set multiple keys to multiple values in a single atomic operation. - * See https://valkey.io/commands/mset/ for details. + * @see {@link https://valkey.io/commands/mset/|valkey.io} for details. * * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * @@ -464,7 +463,7 @@ export class BaseTransaction> { * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or * more keys already exist, the entire operation fails. * - * See https://valkey.io/commands/msetnx/ for more details. + * @see {@link https://valkey.io/commands/msetnx/|valkey.io} for details. * * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * Command Response - `true` if all keys were set. `false` if no key was set. @@ -474,7 +473,7 @@ export class BaseTransaction> { } /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incr/ for details. + * @see {@link https://valkey.io/commands/incr/|valkey.io} for details. * * @param key - The key to increment its value. * @@ -485,7 +484,7 @@ export class BaseTransaction> { } /** Increments the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incrby/ for details. + * @see {@link https://valkey.io/commands/incrby/|valkey.io} for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -499,7 +498,7 @@ export class BaseTransaction> { /** Increment the string representing a floating point number stored at `key` by `amount`. * By using a negative amount value, the result is that the value stored at `key` is decremented. * If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/incrbyfloat/ for details. + * @see {@link https://valkey.io/commands/incrbyfloat/|valkey.io} for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -512,7 +511,7 @@ export class BaseTransaction> { } /** Returns the current connection id. - * See https://valkey.io/commands/client-id/ for details. + * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * * Command Response - the id of the client. */ @@ -521,7 +520,7 @@ export class BaseTransaction> { } /** Decrements the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/decr/ for details. + * @see {@link https://valkey.io/commands/decr/|valkey.io} for details. * * @param key - The key to decrement its value. * @@ -532,7 +531,7 @@ export class BaseTransaction> { } /** Decrements the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/decrby/ for details. + * @see {@link https://valkey.io/commands/decrby/|valkey.io} for details. * * @param key - The key to decrement its value. * @param amount - The amount to decrement. @@ -547,7 +546,7 @@ export class BaseTransaction> { * Perform a bitwise operation between multiple keys (containing string values) and store the result in the * `destination`. * - * See https://valkey.io/commands/bitop/ for more details. + * @see {@link https://valkey.io/commands/bitop/|valkey.io} for details. * * @param operation - The bitwise operation to perform. * @param destination - The key that will store the resulting string. @@ -567,7 +566,7 @@ export class BaseTransaction> { * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. * - * See https://valkey.io/commands/getbit/ for more details. + * @see {@link https://valkey.io/commands/getbit/|valkey.io} for details. * * @param key - The key of the string. * @param offset - The index of the bit to return. @@ -585,7 +584,7 @@ export class BaseTransaction> { * `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to `value` and * the preceding bits are set to `0`. * - * See https://valkey.io/commands/setbit/ for more details. + * @see {@link https://valkey.io/commands/setbit/|valkey.io} for details. * * @param key - The key of the string. * @param offset - The index of the bit to be set. @@ -603,7 +602,7 @@ export class BaseTransaction> { * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being * the last byte of the list, `-2` being the penultimate, and so on. * - * See https://valkey.io/commands/bitpos/ for more details. + * @see {@link https://valkey.io/commands/bitpos/|valkey.io} for details. * * @param key - The key of the string. * @param bit - The bit value to match. Must be `0` or `1`. @@ -627,7 +626,7 @@ export class BaseTransaction> { * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is * specified, `start=0` and `end=2` means to look at the first three bytes. * - * See https://valkey.io/commands/bitpos/ for more details. + * @see {@link https://valkey.io/commands/bitpos/|valkey.io} for details. * * @param key - The key of the string. * @param bit - The bit value to match. Must be `0` or `1`. @@ -654,7 +653,7 @@ export class BaseTransaction> { * Reads or modifies the array of bits representing the string that is held at `key` based on the specified * `subcommands`. * - * See https://valkey.io/commands/bitfield/ for more details. + * @see {@link https://valkey.io/commands/bitfield/|valkey.io} for details. * * @param key - The key of the string. * @param subcommands - The subcommands to be performed on the binary value of the string at `key`, which could be @@ -681,21 +680,21 @@ export class BaseTransaction> { /** * Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. * - * See https://valkey.io/commands/bitfield_ro/ for more details. + * @see {@link https://valkey.io/commands/bitfield_ro/|valkey.io} for details. + * @remarks Since Valkey version 6.0.0. * * @param key - The key of the string. * @param subcommands - The {@link BitFieldGet} subcommands to be performed. * * Command Response - An array of results from the {@link BitFieldGet} subcommands. * - * since Valkey version 6.0.0. */ public bitfieldReadOnly(key: string, subcommands: BitFieldGet[]): T { return this.addAndReturn(createBitField(key, subcommands, true)); } /** Reads the configuration parameters of a running Redis server. - * See https://valkey.io/commands/config-get/ for details. + * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @@ -707,7 +706,7 @@ export class BaseTransaction> { } /** Set configuration parameters to the specified values. - * See https://valkey.io/commands/config-set/ for details. + * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. * @@ -718,7 +717,7 @@ export class BaseTransaction> { } /** Retrieve the value associated with `field` in the hash stored at `key`. - * See https://valkey.io/commands/hget/ for details. + * @see {@link https://valkey.io/commands/hget/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. @@ -730,7 +729,7 @@ export class BaseTransaction> { } /** Sets the specified fields to their respective values in the hash stored at `key`. - * See https://valkey.io/commands/hset/ for details. + * @see {@link https://valkey.io/commands/hset/|valkey.io} for details. * * @param key - The key of the hash. * @param fieldValueMap - A field-value map consisting of fields and their corresponding values @@ -745,7 +744,7 @@ export class BaseTransaction> { /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. - * See https://valkey.io/commands/hsetnx/ for more details. + * @see {@link https://valkey.io/commands/hsetnx/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field to set the value for. @@ -759,7 +758,7 @@ export class BaseTransaction> { /** Removes the specified fields from the hash stored at `key`. * Specified fields that do not exist within this hash are ignored. - * See https://valkey.io/commands/hdel/ for details. + * @see {@link https://valkey.io/commands/hdel/|valkey.io} for details. * * @param key - The key of the hash. * @param fields - The fields to remove from the hash stored at `key`. @@ -772,7 +771,7 @@ export class BaseTransaction> { } /** Returns the values associated with the specified fields in the hash stored at `key`. - * See https://valkey.io/commands/hmget/ for details. + * @see {@link https://valkey.io/commands/hmget/|valkey.io} for details. * * @param key - The key of the hash. * @param fields - The fields in the hash stored at `key` to retrieve from the database. @@ -786,7 +785,7 @@ export class BaseTransaction> { } /** Returns if `field` is an existing field in the hash stored at `key`. - * See https://valkey.io/commands/hexists/ for details. + * @see {@link https://valkey.io/commands/hexists/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field to check in the hash stored at `key`. @@ -799,7 +798,7 @@ export class BaseTransaction> { } /** Returns all fields and values of the hash stored at `key`. - * See https://valkey.io/commands/hgetall/ for details. + * @see {@link https://valkey.io/commands/hgetall/|valkey.io} for details. * * @param key - The key of the hash. * @@ -813,7 +812,7 @@ export class BaseTransaction> { /** Increments the number stored at `field` in the hash stored at `key` by `increment`. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/hincrby/ for details. + * @see {@link https://valkey.io/commands/hincrby/|valkey.io} for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -828,7 +827,7 @@ export class BaseTransaction> { /** Increment the string representing a floating point number stored at `field` in the hash stored at `key` by `increment`. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://valkey.io/commands/hincrbyfloat/ for details. + * @see {@link https://valkey.io/commands/hincrbyfloat/|valkey.io} for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -841,7 +840,7 @@ export class BaseTransaction> { } /** Returns the number of fields contained in the hash stored at `key`. - * See https://valkey.io/commands/hlen/ for more details. + * @see {@link https://valkey.io/commands/hlen/|valkey.io} for details. * * @param key - The key of the hash. * @@ -852,7 +851,7 @@ export class BaseTransaction> { } /** Returns all values in the hash stored at key. - * See https://valkey.io/commands/hvals/ for more details. + * @see {@link https://valkey.io/commands/hvals/|valkey.io} for details. * * @param key - The key of the hash. * @@ -865,7 +864,7 @@ export class BaseTransaction> { /** * Returns the string length of the value associated with `field` in the hash stored at `key`. * - * See https://valkey.io/commands/hstrlen/ for details. + * @see {@link https://valkey.io/commands/hstrlen/|valkey.io} for details. * * @param key - The key of the hash. * @param field - The field in the hash. @@ -879,9 +878,8 @@ export class BaseTransaction> { /** * Returns a random field name from the hash value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @@ -895,7 +893,7 @@ export class BaseTransaction> { /** * Iterates incrementally over a hash. * - * See https://valkey.io/commands/hscan for more details. + * @see {@link https://valkey.io/commands/hscan/|valkey.io} for more details. * * @param key - The key of the set. * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. @@ -914,9 +912,8 @@ export class BaseTransaction> { /** * Retrieves up to `count` random field names from the hash value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @param count - The number of field names to return. @@ -934,9 +931,8 @@ export class BaseTransaction> { * Retrieves up to `count` random field names along with their values from the hash * value stored at `key`. * - * See https://valkey.io/commands/hrandfield/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/hrandfield/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. * @param count - The number of field names to return. @@ -954,7 +950,7 @@ export class BaseTransaction> { /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://valkey.io/commands/lpush/ for details. + * @see {@link https://valkey.io/commands/lpush/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -969,7 +965,7 @@ export class BaseTransaction> { * 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. + * @see {@link https://valkey.io/commands/lpushx/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -982,7 +978,7 @@ export class BaseTransaction> { /** 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. + * @see {@link https://valkey.io/commands/lpop/|valkey.io} for details. * * @param key - The key of the list. * @@ -994,7 +990,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` elements of the list stored at `key`, depending on the list's length. - * See https://valkey.io/commands/lpop/ for details. + * @see {@link https://valkey.io/commands/lpop/|valkey.io} for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -1010,7 +1006,7 @@ export class BaseTransaction> { * 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, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://valkey.io/commands/lrange/ for details. + * @see {@link https://valkey.io/commands/lrange/|valkey.io} for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -1026,7 +1022,7 @@ export class BaseTransaction> { } /** Returns the length of the list stored at `key`. - * See https://valkey.io/commands/llen/ for details. + * @see {@link https://valkey.io/commands/llen/|valkey.io} for details. * * @param key - The key of the list. * @@ -1042,7 +1038,8 @@ export class BaseTransaction> { * depending on `whereFrom`, and pushes the element at the first/last element of the list * stored at `destination` depending on `whereTo`, see {@link ListDirection}. * - * See https://valkey.io/commands/lmove/ for details. + * @see {@link https://valkey.io/commands/lmove/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source list. * @param destination - The key to the destination list. @@ -1050,8 +1047,6 @@ export class BaseTransaction> { * @param whereTo - The {@link ListDirection} to add the element to. * * Command Response - The popped element, or `null` if `source` does not exist. - * - * since Valkey version 6.2.0. */ public lmove( source: string, @@ -1071,11 +1066,10 @@ export class BaseTransaction> { * of the list stored at `destination` depending on `whereTo`. * `BLMOVE` is the blocking variant of {@link lmove}. * - * @remarks - * 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. - * 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. - * - * See https://valkey.io/commands/blmove/ for details. + * @see {@link https://valkey.io/commands/blmove/|valkey.io} for details. + * @remarks When in cluster mode, both `source` and `destination` must map to the same hash slot. + * @remarks `BLMOVE` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands|Valkey Glide Wiki} for more details and best practices. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source list. * @param destination - The key to the destination list. @@ -1084,8 +1078,6 @@ export class BaseTransaction> { * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. * * Command Response - The popped element, or `null` if `source` does not exist or if the operation timed-out. - * - * since Valkey version 6.2.0. */ public blmove( source: string, @@ -1105,7 +1097,7 @@ export class BaseTransaction> { * 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. + * @see {@link https://valkey.io/commands/lset/|valkey.io} for details. * * @param key - The key of the list. * @param index - The index of the element in the list to be set. @@ -1121,7 +1113,7 @@ export class BaseTransaction> { * 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, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://valkey.io/commands/ltrim/ for details. + * @see {@link https://valkey.io/commands/ltrim/|valkey.io} for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -1155,7 +1147,7 @@ export class BaseTransaction> { /** Inserts all the specified values at the tail of the list stored at `key`. * `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://valkey.io/commands/rpush/ for details. + * @see {@link https://valkey.io/commands/rpush/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -1170,7 +1162,7 @@ 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. + * @see {@link https://valkey.io/commands/rpushx/|valkey.io} for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -1183,7 +1175,7 @@ export class BaseTransaction> { /** 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. + * @see {@link https://valkey.io/commands/rpop/|valkey.io} for details. * * @param key - The key of the list. * @@ -1195,7 +1187,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - * See https://valkey.io/commands/rpop/ for details. + * @see {@link https://valkey.io/commands/rpop/|valkey.io} for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -1209,7 +1201,7 @@ export class BaseTransaction> { /** Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. * If `key` does not exist, a new set is created before adding `members`. - * See https://valkey.io/commands/sadd/ for details. + * @see {@link https://valkey.io/commands/sadd/|valkey.io} for details. * * @param key - The key to store the members to its set. * @param members - A list of members to add to the set stored at `key`. @@ -1221,7 +1213,7 @@ export class BaseTransaction> { } /** Removes the specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - * See https://valkey.io/commands/srem/ for details. + * @see {@link https://valkey.io/commands/srem/|valkey.io} for details. * * @param key - The key to remove the members from its set. * @param members - A list of members to remove from the set stored at `key`. @@ -1234,7 +1226,7 @@ export class BaseTransaction> { } /** Returns all the members of the set value stored at `key`. - * See https://valkey.io/commands/smembers/ for details. + * @see {@link https://valkey.io/commands/smembers/|valkey.io} for details. * * @param key - The key to return its members. * @@ -1247,7 +1239,7 @@ export class BaseTransaction> { /** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. * Creates a new destination set if needed. The operation is atomic. - * See https://valkey.io/commands/smove for more details. + * @see {@link https://valkey.io/commands/smove/|valkey.io} for more details. * * @param source - The key of the set to remove the element from. * @param destination - The key of the set to add the element to. @@ -1260,7 +1252,7 @@ export class BaseTransaction> { } /** Returns the set cardinality (number of elements) of the set stored at `key`. - * See https://valkey.io/commands/scard/ for details. + * @see {@link https://valkey.io/commands/scard/|valkey.io} for details. * * @param key - The key to return the number of its members. * @@ -1272,7 +1264,7 @@ export class BaseTransaction> { /** Gets the intersection of all the given sets. * When in cluster mode, all `keys` must map to the same hash slot. - * See https://valkey.io/docs/latest/commands/sinter/ for more details. + * @see {@link https://valkey.io/commands/sinter/|valkey.io} for details. * * @param keys - The `keys` of the sets to get the intersection. * @@ -1286,13 +1278,12 @@ export class BaseTransaction> { /** * Gets the cardinality of the intersection of all the given sets. * - * See https://valkey.io/commands/sintercard/ for more details. + * @see {@link https://valkey.io/commands/sintercard/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @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)); @@ -1301,7 +1292,7 @@ export class BaseTransaction> { /** * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. * - * See https://valkey.io/commands/sinterstore/ for more details. + * @see {@link https://valkey.io/commands/sinterstore/|valkey.io} for details. * * @param destination - The key of the destination set. * @param keys - The keys from which to retrieve the set members. @@ -1315,7 +1306,7 @@ export class BaseTransaction> { /** * Computes the difference between the first set and all the successive sets in `keys`. * - * See https://valkey.io/commands/sdiff/ for more details. + * @see {@link https://valkey.io/commands/sdiff/|valkey.io} for details. * * @param keys - The keys of the sets to diff. * @@ -1329,7 +1320,7 @@ export class BaseTransaction> { /** * Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. * - * See https://valkey.io/commands/sdiffstore/ for more details. + * @see {@link https://valkey.io/commands/sdiffstore/|valkey.io} for details. * * @param destination - The key of the destination set. * @param keys - The keys of the sets to diff. @@ -1343,7 +1334,7 @@ export class BaseTransaction> { /** * Gets the union of all the given sets. * - * See https://valkey.io/commands/sunion/ for more details. + * @see {@link https://valkey.io/commands/sunion/|valkey.io} for details. * * @param keys - The keys of the sets. * @@ -1358,7 +1349,7 @@ export class BaseTransaction> { * Stores the members of the union of all given sets specified by `keys` into a new set * at `destination`. * - * See https://valkey.io/commands/sunionstore/ for details. + * @see {@link https://valkey.io/commands/sunionstore/|valkey.io} for details. * * @param destination - The key of the destination set. * @param keys - The keys from which to retrieve the set members. @@ -1370,7 +1361,7 @@ export class BaseTransaction> { } /** Returns if `member` is a member of the set stored at `key`. - * See https://valkey.io/commands/sismember/ for more details. + * @see {@link https://valkey.io/commands/sismember/|valkey.io} for details. * * @param key - The key of the set. * @param member - The member to check for existence in the set. @@ -1385,21 +1376,20 @@ export class BaseTransaction> { /** * Checks whether each member is contained in the members of the set stored at `key`. * - * See https://valkey.io/commands/smismember/ for more details. + * @see {@link https://valkey.io/commands/smismember/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @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. + * @see {@link https://valkey.io/commands/spop/|valkey.io} for details. * To pop multiple members, see `spopCount`. * * @param key - The key of the set. @@ -1412,7 +1402,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` random members from the set value store at `key`, depending on the set's length. - * See https://valkey.io/commands/spop/ for details. + * @see {@link https://valkey.io/commands/spop/|valkey.io} for details. * * @param key - The key of the set. * @param count - The count of the elements to pop from the set. @@ -1426,7 +1416,7 @@ export class BaseTransaction> { /** Returns a random element from the set value stored at `key`. * - * See https://valkey.io/commands/srandmember for more details. + * @see {@link https://valkey.io/commands/srandmember/|valkey.io} for more details. * * @param key - The key from which to retrieve the set member. * Command Response - A random element from the set, or null if `key` does not exist. @@ -1437,7 +1427,7 @@ export class BaseTransaction> { /** Returns one or more random elements from the set value stored at `key`. * - * See https://valkey.io/commands/srandmember for more details. + * @see {@link https://valkey.io/commands/srandmember/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - The number of members to return. @@ -1450,7 +1440,7 @@ export class BaseTransaction> { } /** Returns the number of keys in `keys` that exist in the database. - * See https://valkey.io/commands/exists/ for details. + * @see {@link https://valkey.io/commands/exists/|valkey.io} for details. * * @param keys - The keys list to check. * @@ -1464,7 +1454,7 @@ export class BaseTransaction> { /** Removes the specified keys. A key is ignored if it does not exist. * This command, similar to DEL, removes specified keys and ignores non-existent ones. * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. - * See https://valkey.io/commands/unlink/ for details. + * @see {@link https://valkey.io/commands/unlink/|valkey.io} for details. * * @param keys - The keys we wanted to unlink. * @@ -1478,7 +1468,7 @@ export class BaseTransaction> { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/expire/ for details. + * @see {@link https://valkey.io/commands/expire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. @@ -1495,7 +1485,7 @@ export class BaseTransaction> { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/expireat/ for details. + * @see {@link https://valkey.io/commands/expireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. @@ -1516,13 +1506,12 @@ export class BaseTransaction> { * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. * To get the expiration with millisecond precision, use {@link pexpiretime}. * - * See https://valkey.io/commands/expiretime/ for details. + * @see {@link https://valkey.io/commands/expiretime/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. - * - * since Valkey version 7.0.0. */ public expireTime(key: string): T { return this.addAndReturn(createExpireTime(key)); @@ -1532,7 +1521,7 @@ export class BaseTransaction> { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/pexpire/ for details. + * @see {@link https://valkey.io/commands/pexpire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. @@ -1553,7 +1542,7 @@ export class BaseTransaction> { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://valkey.io/commands/pexpireat/ for details. + * @see {@link https://valkey.io/commands/pexpireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. @@ -1575,20 +1564,19 @@ export class BaseTransaction> { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. * - * See https://valkey.io/commands/pexpiretime/ for details. + * @see {@link https://valkey.io/commands/pexpiretime/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. - * - * since Valkey version 7.0.0. */ public pexpireTime(key: string): T { return this.addAndReturn(createPExpireTime(key)); } /** Returns the remaining time to live of `key` that has a timeout. - * See https://valkey.io/commands/ttl/ for details. + * @see {@link https://valkey.io/commands/ttl/|valkey.io} for details. * * @param key - The key to return its timeout. * @@ -1600,7 +1588,7 @@ export class BaseTransaction> { /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. - * See https://valkey.io/commands/zadd/ for more details. + * @see {@link https://valkey.io/commands/zadd/|valkey.io} for details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. @@ -1620,7 +1608,7 @@ export class BaseTransaction> { /** Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. - * See https://valkey.io/commands/zadd/ for more details. + * @see {@link https://valkey.io/commands/zadd/|valkey.io} for details. * * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. @@ -1643,7 +1631,7 @@ export class BaseTransaction> { /** Removes the specified members from the sorted set stored at `key`. * Specified members that are not a member of this set are ignored. - * See https://valkey.io/commands/zrem/ for more details. + * @see {@link https://valkey.io/commands/zrem/|valkey.io} for details. * * @param key - The key of the sorted set. * @param members - A list of members to remove from the sorted set. @@ -1656,7 +1644,7 @@ export class BaseTransaction> { } /** Returns the cardinality (number of elements) of the sorted set stored at `key`. - * See https://valkey.io/commands/zcard/ for more details. + * @see {@link https://valkey.io/commands/zcard/|valkey.io} for details. * * @param key - The key of the sorted set. * @@ -1670,15 +1658,14 @@ export class BaseTransaction> { /** * Returns the cardinality of the intersection of the sorted sets specified by `keys`. * - * See https://valkey.io/commands/zintercard/ for more details. + * @see {@link https://valkey.io/commands/zintercard/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param keys - The keys of the sorted sets to intersect. * @param limit - An optional argument that can be used to specify a maximum number for the * intersection cardinality. If limit is not supplied, or if it is set to `0`, there will be no limit. * * Command Response - The cardinality of the intersection of the given sorted sets. - * - * since - Redis version 7.0.0. */ public zintercard(keys: string[], limit?: number): T { return this.addAndReturn(createZInterCard(keys, limit)); @@ -1688,14 +1675,13 @@ export class BaseTransaction> { * Returns the difference between the first sorted set and all the successive sorted sets. * To get the elements with their scores, see {@link zdiffWithScores}. * - * See https://valkey.io/commands/zdiff/ for more details. + * @see {@link https://valkey.io/commands/zdiff/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets. * * Command Response - An `array` of elements representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. - * - * since Valkey version 6.2.0. */ public zdiff(keys: string[]): T { return this.addAndReturn(createZDiff(keys)); @@ -1705,14 +1691,13 @@ export class BaseTransaction> { * Returns the difference between the first sorted set and all the successive sorted sets, with the associated * scores. * - * See https://valkey.io/commands/zdiff/ for more details. + * @see {@link https://valkey.io/commands/zdiff/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets. * * Command Response - A map of elements and their scores representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. - * - * since Valkey version 6.2.0. */ public zdiffWithScores(keys: string[]): T { return this.addAndReturn(createZDiffWithScores(keys)); @@ -1723,21 +1708,20 @@ export class BaseTransaction> { * the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are * treated as empty sets. * - * See https://valkey.io/commands/zdiffstore/ for more details. + * @see {@link https://valkey.io/commands/zdiffstore/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param destination - The key for the resulting sorted set. * @param keys - The keys of the sorted sets to compare. * * Command Response - The number of members in the resulting sorted set stored at `destination`. - * - * since Valkey version 6.2.0. */ public zdiffstore(destination: string, keys: string[]): T { return this.addAndReturn(createZDiffStore(destination, keys)); } /** Returns the score of `member` in the sorted set stored at `key`. - * See https://valkey.io/commands/zscore/ for more details. + * @see {@link https://valkey.io/commands/zscore/|valkey.io} for details. * * @param key - The key of the sorted set. * @param member - The member whose score is to be retrieved. @@ -1753,22 +1737,21 @@ export class BaseTransaction> { /** * Returns the scores associated with the specified `members` in the sorted set stored at `key`. * - * See https://valkey.io/commands/zmscore/ for more details. + * @see {@link https://valkey.io/commands/zmscore/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the sorted set. * @param members - A list of members in the sorted set. * * Command Response - An `array` of scores corresponding to `members`. * If a member does not exist in the sorted set, the corresponding value in the list will be `null`. - * - * since Valkey version 6.2.0. */ public zmscore(key: string, members: string[]): T { return this.addAndReturn(createZMScore(key, members)); } /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. - * See https://valkey.io/commands/zcount/ for more details. + * @see {@link https://valkey.io/commands/zcount/|valkey.io} for details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. @@ -1789,7 +1772,7 @@ export class BaseTransaction> { /** Returns the specified range of elements in the sorted set stored at `key`. * ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. * - * See https://valkey.io/commands/zrange/ for more details. + * @see {@link https://valkey.io/commands/zrange/|valkey.io} for details. * To get the elements with their scores, see `zrangeWithScores`. * * @param key - The key of the sorted set. @@ -1812,7 +1795,7 @@ export class BaseTransaction> { /** Returns the specified range of elements with their scores in the sorted set stored at `key`. * Similar to ZRANGE but with a WITHSCORE flag. - * See https://valkey.io/commands/zrange/ for more details. + * @see {@link https://valkey.io/commands/zrange/|valkey.io} for details. * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -1839,7 +1822,8 @@ export class BaseTransaction> { * sorted set at `destination`. If `destination` doesn't exist, a new sorted * set is created; if it exists, it's overwritten. * - * See https://valkey.io/commands/zrangestore/ for more details. + * @see {@link https://valkey.io/commands/zrangestore/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param destination - The key for the destination sorted set. * @param source - The key of the source sorted set. @@ -1850,8 +1834,6 @@ export class BaseTransaction> { * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * * Command Response - The number of elements in the resulting sorted set. - * - * since - Redis version 6.2.0. */ public zrangeStore( destination: string, @@ -1870,7 +1852,7 @@ export class BaseTransaction> { * * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. * - * See https://valkey.io/commands/zinterstore/ for more details. + * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for details. * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: @@ -1892,7 +1874,7 @@ export class BaseTransaction> { /** * Returns a random member from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for details. * * @param keys - The key of the sorted set. * Command Response - A string representing a random member from the sorted set. @@ -1905,7 +1887,7 @@ export class BaseTransaction> { /** * Returns random members from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for details. * * @param keys - The key of the sorted set. * @param count - The number of members to return. @@ -1921,7 +1903,7 @@ export class BaseTransaction> { /** * Returns random members with scores from the sorted set stored at `key`. * - * See https://valkey.io/commands/zrandmember/ for more details. + * @see {@link https://valkey.io/commands/zrandmember/|valkey.io} for details. * * @param keys - The key of the sorted set. * @param count - The number of members to return. @@ -1936,7 +1918,7 @@ export class BaseTransaction> { } /** Returns the string representation of the type of the value stored at `key`. - * See https://valkey.io/commands/type/ for more details. + * @see {@link https://valkey.io/commands/type/|valkey.io} for details. * * @param key - The key to check its data type. * @@ -1947,7 +1929,7 @@ export class BaseTransaction> { } /** Returns the length of the string value stored at `key`. - * See https://valkey.io/commands/strlen/ for more details. + * @see {@link https://valkey.io/commands/strlen/|valkey.io} for details. * * @param key - The `key` to check its length. * @@ -1961,7 +1943,7 @@ export class BaseTransaction> { /** Removes and returns the members with the lowest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the lowest scores are removed and returned. * Otherwise, only one member with the lowest score is removed and returned. - * See https://valkey.io/commands/zpopmin for more details. + * @see {@link https://valkey.io/commands/zpopmin/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -1980,11 +1962,11 @@ export class BaseTransaction> { * are provided. * `BZPOPMIN` is the blocking variant of {@link zpopmin}. * - * See https://valkey.io/commands/bzpopmin/ for more details. + * @see {@link https://valkey.io/commands/bzpopmin/|valkey.io} for details. * * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of - * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * `0` will block indefinitely. Since Valkey version 6.0.0: timeout is interpreted as a double instead of an integer. * * Command Response - An `array` containing the key where the member was popped out, the member, itself, and the member score. * If no member could be popped and the `timeout` expired, returns `null`. @@ -1996,7 +1978,7 @@ export class BaseTransaction> { /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. - * See https://valkey.io/commands/zpopmax for more details. + * @see {@link https://valkey.io/commands/zpopmax/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -2015,7 +1997,7 @@ export class BaseTransaction> { * are provided. * `BZPOPMAX` is the blocking variant of {@link zpopmax}. * - * See https://valkey.io/commands/bzpopmax/ for more details. + * @see {@link https://valkey.io/commands/bzpopmax/|valkey.io} for details. * * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of @@ -2029,7 +2011,7 @@ export class BaseTransaction> { } /** Echoes the provided `message` back. - * See https://valkey.io/commands/echo for more details. + * @see {@link https://valkey.io/commands/echo/|valkey.io} for more details. * * @param message - The message to be echoed back. * @@ -2040,7 +2022,7 @@ export class BaseTransaction> { } /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. - * See https://valkey.io/commands/pttl for more details. + * @see {@link https://valkey.io/commands/pttl/|valkey.io} for more details. * * @param key - The key to return its timeout. * @@ -2053,7 +2035,7 @@ export class BaseTransaction> { /** Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. * Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. * These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. - * See https://valkey.io/commands/zremrangebyrank/ for more details. + * @see {@link https://valkey.io/commands/zremrangebyrank/|valkey.io} for details. * * @param key - The key of the sorted set. * @param start - The starting point of the range. @@ -2071,7 +2053,7 @@ export class BaseTransaction> { /** * Removes all elements in the sorted set stored at `key` with lexicographical order between `minLex` and `maxLex`. * - * See https://valkey.io/commands/zremrangebylex/ for more details. + * @see {@link https://valkey.io/commands/zremrangebylex/|valkey.io} for details. * * @param key - The key of the sorted set. * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. @@ -2090,7 +2072,7 @@ export class BaseTransaction> { } /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. - * See https://valkey.io/commands/zremrangebyscore/ for more details. + * @see {@link https://valkey.io/commands/zremrangebyscore/|valkey.io} for details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to remove from. Can be positive/negative infinity, or specific score and inclusivity. @@ -2113,7 +2095,7 @@ export class BaseTransaction> { /** * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. * - * See https://valkey.io/commands/zlexcount/ for more details. + * @see {@link https://valkey.io/commands/zlexcount/|valkey.io} for details. * * @param key - The key of the sorted set. * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. @@ -2132,7 +2114,7 @@ export class BaseTransaction> { } /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. - * See https://valkey.io/commands/zrank for more details. + * @see {@link https://valkey.io/commands/zrank/|valkey.io} for more details. * To get the rank of `member` with its score, see `zrankWithScore`. * * @param key - The key of the sorted set. @@ -2146,15 +2128,15 @@ export class BaseTransaction> { } /** Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. - * See https://valkey.io/commands/zrank for more details. + * + * @see {@link https://valkey.io/commands/zrank/|valkey.io} for more details. + * @remarks Since Valkey version 7.2.0. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. * * Command Response - A list containing the rank and score of `member` in the sorted set. * If `key` doesn't exist, or if `member` is not present in the set, null will be returned. - * - * since - Redis version 7.2.0. */ public zrankWithScore(key: string, member: string): T { return this.addAndReturn(createZRank(key, member, true)); @@ -2165,7 +2147,7 @@ export class BaseTransaction> { * scores are ordered from the highest to lowest, starting from 0. * To get the rank of `member` with its score, see {@link zrevrankWithScore}. * - * See https://valkey.io/commands/zrevrank/ for more details. + * @see {@link https://valkey.io/commands/zrevrank/|valkey.io} for details. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -2181,7 +2163,8 @@ export class BaseTransaction> { * Returns the rank of `member` in the sorted set stored at `key` with its * score, where scores are ordered from the highest to lowest, starting from 0. * - * See https://valkey.io/commands/zrevrank/ for more details. + * @see {@link https://valkey.io/commands/zrevrank/|valkey.io} for details. + * @remarks Since Valkey version 7.2.0. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -2189,8 +2172,6 @@ export class BaseTransaction> { * Command Response - A list containing the rank and score of `member` in the sorted set, where ranks * are ordered from high to low based on scores. * If `key` doesn't exist, or if `member` is not present in the set, `null` will be returned. - * - * since - Valkey version 7.2.0. */ public zrevrankWithScore(key: string, member: string): T { return this.addAndReturn(createZRevRankWithScore(key, member)); @@ -2198,7 +2179,7 @@ export class BaseTransaction> { /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). - * See https://valkey.io/commands/persist/ for more details. + * @see {@link https://valkey.io/commands/persist/|valkey.io} for details. * * @param key - The key to remove the existing timeout on. * @@ -2211,8 +2192,7 @@ export class BaseTransaction> { /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * - * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) - * for details on the restrictions and limitations of the custom command API. + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command|Valkey Glide Wiki} for details on the restrictions and limitations of the custom command API. * * Command Response - A response from Redis with an `Object`. */ @@ -2224,7 +2204,7 @@ export class BaseTransaction> { * 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/lindex/ for more details. + * @see {@link https://valkey.io/commands/lindex/|valkey.io} for details. * * @param key - The `key` of the list. * @param index - The `index` of the element in the list to retrieve. @@ -2238,7 +2218,7 @@ export class BaseTransaction> { /** * Inserts `element` in the list at `key` either before or after the `pivot`. * - * See https://valkey.io/commands/linsert/ for more details. + * @see {@link https://valkey.io/commands/linsert/|valkey.io} for details. * * @param key - The key of the list. * @param position - The relative position to insert into - either `InsertPosition.Before` or @@ -2261,7 +2241,7 @@ export class BaseTransaction> { /** * Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - * See https://valkey.io/commands/xadd/ for more details. + * @see {@link https://valkey.io/commands/xadd/|valkey.io} for details. * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. @@ -2280,7 +2260,7 @@ export class BaseTransaction> { /** * Removes the specified entries by id from a stream, and returns the number of entries deleted. * - * See https://valkey.io/commands/xdel for more details. + * @see {@link https://valkey.io/commands/xdel/|valkey.io} for more details. * * @param key - The key of the stream. * @param ids - An array of entry ids. @@ -2294,7 +2274,7 @@ export class BaseTransaction> { /** * Trims the stream stored at `key` by evicting older entries. - * See https://valkey.io/commands/xtrim/ for more details. + * @see {@link https://valkey.io/commands/xtrim/|valkey.io} for details. * * @param key - the key of the stream * @param options - options detailing how to trim the stream. @@ -2321,7 +2301,7 @@ export class BaseTransaction> { } /** Returns the server time. - * See https://valkey.io/commands/time/ for details. + * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * * Command Response - The current server time as a two items `array`: * A Unix timestamp and the amount of microseconds already elapsed in the current second. @@ -2334,7 +2314,7 @@ export class BaseTransaction> { /** * Returns stream entries matching a given range of entry IDs. * - * See https://valkey.io/commands/xrange for more details. + * @see {@link https://valkey.io/commands/xrange/|valkey.io} for more details. * * @param key - The key of the stream. * @param start - The starting stream entry ID bound for the range. @@ -2361,7 +2341,7 @@ export class BaseTransaction> { /** * Reads entries from the given streams. - * See https://valkey.io/commands/xread/ for more details. + * @see {@link https://valkey.io/commands/xread/|valkey.io} for details. * * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. * @param options - options detailing how to read the stream. @@ -2378,7 +2358,7 @@ export class BaseTransaction> { /** * Returns the number of entries in the stream stored at `key`. * - * See https://valkey.io/commands/xlen/ for more details. + * @see {@link https://valkey.io/commands/xlen/|valkey.io} for details. * * @param key - The key of the stream. * @@ -2391,11 +2371,11 @@ export class BaseTransaction> { /** * Returns stream message summary information for pending messages matching a given range of IDs. * - * See https://valkey.io/commands/xpending/ for more details. + * @see {@link https://valkey.io/commands/xpending/|valkey.io} for details. * Returns the list of all consumers and their attributes for the given consumer group of the * stream stored at `key`. * - * See https://valkey.io/commands/xinfo-consumers/ for more details. + * @see {@link https://valkey.io/commands/xinfo-consumers/|valkey.io} for details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2410,7 +2390,7 @@ export class BaseTransaction> { /** * Returns stream message summary information for pending messages matching a given range of IDs. * - * See https://valkey.io/commands/xpending/ for more details. + * @see {@link https://valkey.io/commands/xpending/|valkey.io} for details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2431,7 +2411,7 @@ export class BaseTransaction> { * Returns the list of all consumers and their attributes for the given consumer group of the * stream stored at `key`. * - * See https://valkey.io/commands/xinfo-consumers/ for more details. + * @see {@link https://valkey.io/commands/xinfo-consumers/|valkey.io} for details. * * Command Response - An `Array` of `Records`, where each mapping contains the attributes * of a consumer for the given consumer group of the stream at `key`. @@ -2443,7 +2423,7 @@ export class BaseTransaction> { /** * Changes the ownership of a pending message. * - * See https://valkey.io/commands/xclaim/ for more details. + * @see {@link https://valkey.io/commands/xclaim/|valkey.io} for details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2471,7 +2451,7 @@ export class BaseTransaction> { * Changes the ownership of a pending message. This function returns an `array` with * only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. * - * See https://valkey.io/commands/xclaim/ for more details. + * @see {@link https://valkey.io/commands/xclaim/|valkey.io} for details. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2498,9 +2478,8 @@ export class BaseTransaction> { /** * Transfers ownership of pending stream entries that match the specified criteria. * - * See https://valkey.io/commands/xautoclaim/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/xautoclaim/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2535,9 +2514,8 @@ export class BaseTransaction> { /** * Transfers ownership of pending stream entries that match the specified criteria. * - * See https://valkey.io/commands/xautoclaim/ for more details. - * - * since Valkey version 6.2.0. + * @see {@link https://valkey.io/commands/xautoclaim/|valkey.io} for more details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the stream. * @param group - The consumer group name. @@ -2581,7 +2559,7 @@ export class BaseTransaction> { * Creates a new consumer group uniquely identified by `groupname` for the stream * stored at `key`. * - * See https://valkey.io/commands/xgroup-create/ for more details. + * @see {@link https://valkey.io/commands/xgroup-create/|valkey.io} for details. * * @param key - The key of the stream. * @param groupName - The newly created consumer group name. @@ -2604,7 +2582,7 @@ export class BaseTransaction> { /** * Destroys the consumer group `groupname` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-destroy/ for more details. + * @see {@link https://valkey.io/commands/xgroup-destroy/|valkey.io} for details. * * @param key - The key of the stream. * @param groupname - The newly created consumer group name. @@ -2618,7 +2596,7 @@ export class BaseTransaction> { /** * Creates a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-createconsumer for more details. + * @see {@link https://valkey.io/commands/xgroup-createconsumer/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupName - The consumer group name. @@ -2639,7 +2617,7 @@ export class BaseTransaction> { /** * Deletes a consumer named `consumerName` in the consumer group `groupName` for the stream stored at `key`. * - * See https://valkey.io/commands/xgroup-delconsumer for more details. + * @see {@link https://valkey.io/commands/xgroup-delconsumer/|valkey.io} for more details. * * @param key - The key of the stream. * @param groupName - The consumer group name. @@ -2662,10 +2640,12 @@ export class BaseTransaction> { * If `newkey` already exists it is overwritten. * In Cluster mode, both `key` and `newkey` must be in the same hash slot, * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - * See https://valkey.io/commands/rename/ for more details. + * + * @see {@link https://valkey.io/commands/rename/|valkey.io} for details. * * @param key - The key to rename. * @param newKey - The new name of the key. + * * Command Response - If the `key` was successfully renamed, return "OK". If `key` does not exist, an error is thrown. */ public rename(key: string, newKey: string): T { @@ -2676,7 +2656,8 @@ export class BaseTransaction> { * Renames `key` to `newkey` if `newkey` does not yet exist. * In Cluster mode, both `key` and `newkey` must be in the same hash slot, * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - * See https://valkey.io/commands/renamenx/ for more details. + * + * @see {@link https://valkey.io/commands/renamenx/|valkey.io} for details. * * @param key - The key to rename. * @param newKey - The new name of the key. @@ -2691,12 +2672,13 @@ export class BaseTransaction> { * Pop an element from the tail of the first list that is non-empty, * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://valkey.io/commands/brpop/ for more details. - * Note: `BRPOP` is a blocking command, - * see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * + * @see {@link https://valkey.io/commands/brpop/|valkey.io} for details. + * @remarks `BRPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. + * * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. */ @@ -2708,12 +2690,13 @@ export class BaseTransaction> { * Pop an element from the head of the first list that is non-empty, * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://valkey.io/commands/blpop/ for more details. - * Note: `BLPOP` is a blocking command, - * see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * + * @see {@link https://valkey.io/commands/blpop/|valkey.io} for details. + * @remarks `BLPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. + * * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. */ @@ -2725,7 +2708,7 @@ export class BaseTransaction> { * Creates a new structure if the `key` does not exist. * When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. * - * See https://valkey.io/commands/pfadd/ for more details. + * @see {@link https://valkey.io/commands/pfadd/|valkey.io} for details. * * @param key - The key of the HyperLogLog data structure to add elements into. * @param elements - An array of members to add to the HyperLogLog stored at `key`. @@ -2739,7 +2722,7 @@ export class BaseTransaction> { /** Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. * - * See https://valkey.io/commands/pfcount/ for more details. + * @see {@link https://valkey.io/commands/pfcount/|valkey.io} for details. * * @param keys - The keys of the HyperLogLog data structures to be analyzed. * Command Response - The approximated cardinality of given HyperLogLog data structures. @@ -2753,7 +2736,7 @@ export class BaseTransaction> { * Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is * treated as one of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. * - * See https://valkey.io/commands/pfmerge/ for more details. + * @see {@link https://valkey.io/commands/pfmerge/|valkey.io} for details. * * @param destination - The key of the destination HyperLogLog where the merged data sets will be stored. * @param sourceKeys - The keys of the HyperLogLog structures to be merged. @@ -2765,7 +2748,7 @@ export class BaseTransaction> { /** Returns the internal encoding for the Redis object stored at `key`. * - * See https://valkey.io/commands/object-encoding for more details. + * @see {@link https://valkey.io/commands/object-encoding/|valkey.io} for more details. * * @param key - The `key` of the object to get the internal encoding of. * Command Response - If `key` exists, returns the internal encoding of the object stored at `key` as a string. @@ -2777,7 +2760,7 @@ export class BaseTransaction> { /** Returns the logarithmic access frequency counter of a Redis object stored at `key`. * - * See https://valkey.io/commands/object-freq for more details. + * @see {@link https://valkey.io/commands/object-freq/|valkey.io} for more details. * * @param key - The `key` of the object to get the logarithmic access frequency counter of. * Command Response - If `key` exists, returns the logarithmic access frequency counter of @@ -2790,7 +2773,7 @@ export class BaseTransaction> { /** * Returns the time in seconds since the last access to the value stored at `key`. * - * See https://valkey.io/commands/object-idletime/ for more details. + * @see {@link https://valkey.io/commands/object-idletime/|valkey.io} for details. * * @param key - The key of the object to get the idle time of. * @@ -2803,7 +2786,7 @@ export class BaseTransaction> { /** * Returns the reference count of the object stored at `key`. * - * See https://valkey.io/commands/object-refcount/ for more details. + * @see {@link https://valkey.io/commands/object-refcount/|valkey.io} for details. * * @param key - The `key` of the object to get the reference count of. * @@ -2817,7 +2800,7 @@ export class BaseTransaction> { /** * Displays a piece of generative computer art and the server version. * - * See https://valkey.io/commands/lolwut/ for more details. + * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for details. * * @param options - The LOLWUT options. * @@ -2832,7 +2815,7 @@ export class BaseTransaction> { * acknowledged by at least `numreplicas` of replicas. If `timeout` is reached, the command returns * the number of replicas that were not yet reached. * - * See https://valkey.io/commands/wait/ for more details. + * @see {@link https://valkey.io/commands/wait/|valkey.io} for more details. * * @param numreplicas - The number of replicas to reach. * @param timeout - The timeout value specified in milliseconds. A value of 0 will block indefinitely. @@ -2847,9 +2830,8 @@ export class BaseTransaction> { /** * Invokes a previously loaded function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, @@ -2865,9 +2847,8 @@ export class BaseTransaction> { /** * Invokes a previously loaded read-only function. * - * See https://valkey.io/commands/fcall/ for more details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param func - The function name. * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, @@ -2883,9 +2864,8 @@ export class BaseTransaction> { /** * Deletes a library and all its functions. * - * See https://valkey.io/commands/function-delete/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-delete/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The library name to delete. * @@ -2898,9 +2878,8 @@ export class BaseTransaction> { /** * Loads a library to Valkey. * - * See https://valkey.io/commands/function-load/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-load/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. * @param replace - Whether the given library should overwrite a library with the same name if it @@ -2915,9 +2894,8 @@ export class BaseTransaction> { /** * Deletes all function libraries. * - * See https://valkey.io/commands/function-flush/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * Command Response - `OK`. @@ -2929,9 +2907,8 @@ export class BaseTransaction> { /** * Returns information about the functions and libraries. * - * See https://valkey.io/commands/function-list/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param options - Parameters to filter and request additional info. * @@ -2945,9 +2922,8 @@ export class BaseTransaction> { * Returns information about the function that's currently running and information about the * available execution engines. * - * See https://valkey.io/commands/function-stats/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * Command Response - A `Record` of type {@link FunctionStatsResponse} with two keys: * @@ -2961,7 +2937,7 @@ export class BaseTransaction> { /** * Deletes all the keys of all the existing databases. This command never fails. * - * See https://valkey.io/commands/flushall/ for more details. + * @see {@link https://valkey.io/commands/flushall/|valkey.io} for details. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @@ -2974,7 +2950,7 @@ export class BaseTransaction> { /** * Deletes all the keys of the currently selected database. This command never fails. * - * See https://valkey.io/commands/flushdb/ for more details. + * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for details. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * @@ -2989,7 +2965,8 @@ export class BaseTransaction> { * 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. + * @see {@link https://valkey.io/commands/lpos/|valkey.io} for details. + * @remarks Since Valkey version 6.0.6. * * @param key - The name of the list. * @param element - The value to search for within the list. @@ -2997,8 +2974,6 @@ export class BaseTransaction> { * * 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)); @@ -3007,7 +2982,7 @@ export class BaseTransaction> { /** * Returns the number of keys in the currently selected database. * - * See https://valkey.io/commands/dbsize/ for more details. + * @see {@link https://valkey.io/commands/dbsize/|valkey.io} for details. * * Command Response - The number of keys in the currently selected database. */ @@ -3019,7 +2994,7 @@ export class BaseTransaction> { * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can * optionally be provided to count the number of bits in a specific string interval. * - * See https://valkey.io/commands/bitcount for more details. + * @see {@link https://valkey.io/commands/bitcount/|valkey.io} for more details. * * @param key - The key for the string to count the set bits of. * @param options - The offset options. @@ -3036,7 +3011,7 @@ export class BaseTransaction> { * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. * - * See https://valkey.io/commands/geoadd/ for more details. + * @see {@link https://valkey.io/commands/geoadd/|valkey.io} for details. * * @param key - The key of the sorted set. * @param membersToGeospatialData - A mapping of member names to their corresponding positions - see @@ -3061,9 +3036,8 @@ export class BaseTransaction> { * Returns the members of a sorted set populated with geospatial information using {@link geoadd}, * which are within the borders of the area specified by a given shape. * - * See https://valkey.io/commands/geosearch/ for more details. - * - * since - Valkey 6.2.0 and above. + * @see {@link https://valkey.io/commands/geosearch/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param key - The key of the sorted set. * @param searchFrom - The query's center point options, could be one of: @@ -3108,9 +3082,8 @@ export class BaseTransaction> { * * To get the result directly, see {@link geosearch}. * - * See https://valkey.io/commands/geosearchstore/ for more details. - * - * since - Valkey 6.2.0 and above. + * @see {@link https://valkey.io/commands/geosearchstore/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param destination - The key of the destination sorted set. * @param source - The key of the sorted set. @@ -3146,7 +3119,7 @@ export class BaseTransaction> { * Returns the positions (longitude, latitude) of all the specified `members` of the * geospatial index represented by the sorted set at `key`. * - * See https://valkey.io/commands/geopos for more details. + * @see {@link https://valkey.io/commands/geopos/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param members - The members for which to get the positions. @@ -3163,7 +3136,8 @@ export class BaseTransaction> { * Pops a member-score pair from the first non-empty sorted set, with the given `keys` * being checked in the order they are provided. * - * See https://valkey.io/commands/zmpop/ for more details. + * @see {@link https://valkey.io/commands/zmpop/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or @@ -3173,8 +3147,6 @@ export class BaseTransaction> { * Command Response - A two-element `array` containing the key name of the set from which the * element was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. - * - * since Valkey version 7.0.0. */ public zmpop(keys: string[], modifier: ScoreFilter, count?: number): T { return this.addAndReturn(createZMPop(keys, modifier, count)); @@ -3185,10 +3157,10 @@ export class BaseTransaction> { * checked in the order they are provided. Blocks the connection when there are no members * to pop from any of the given sorted sets. `BZMPOP` is the blocking variant of {@link zmpop}. * - * See https://valkey.io/commands/bzmpop/ for more details. + * @see {@link https://valkey.io/commands/bzmpop/|valkey.io} for details. + * @remarks `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | Valkey Glide Wiki} for more details and best practices. + * @remarks Since Valkey version 7.0.0. * - * @remarks `BZMPOP` is a client blocking command, see {@link https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands | the wiki} - * for more details and best practices. * @param keys - The keys of the sorted sets. * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. @@ -3198,8 +3170,6 @@ export class BaseTransaction> { * Command Response - A two-element `array` containing the key name of the set from which the element * was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. - * - * since Valkey version 7.0.0. */ public bzmpop( keys: string[], @@ -3215,7 +3185,7 @@ export class BaseTransaction> { * If `member` does not exist in the sorted set, it is added with `increment` as its score. * If `key` does not exist, a new sorted set is created with the specified member as its sole member. * - * See https://valkey.io/commands/zincrby/ for details. + * @see {@link https://valkey.io/commands/zincrby/|valkey.io} for details. * * @param key - The key of the sorted set. * @param increment - The score increment. @@ -3230,7 +3200,7 @@ export class BaseTransaction> { /** * Iterates incrementally over a sorted set. * - * See https://valkey.io/commands/zscan for more details. + * @see {@link https://valkey.io/commands/zscan/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of @@ -3250,7 +3220,7 @@ export class BaseTransaction> { /** * Returns the distance between `member1` and `member2` saved in the geospatial index stored at `key`. * - * See https://valkey.io/commands/geodist/ for more details. + * @see {@link https://valkey.io/commands/geodist/|valkey.io} for details. * * @param key - The key of the sorted set. * @param member1 - The name of the first member. @@ -3272,7 +3242,7 @@ export class BaseTransaction> { /** * Returns the `GeoHash` strings representing the positions of all the specified `members` in the sorted set stored at `key`. * - * See https://valkey.io/commands/geohash/ for more details. + * @see {@link https://valkey.io/commands/geohash/|valkey.io} for details. * * @param key - The key of the sorted set. * @param members - The array of members whose `GeoHash` strings are to be retrieved. @@ -3288,7 +3258,7 @@ export class BaseTransaction> { * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. * - * See https://valkey.io/commands/lastsave/ for more details. + * @see {@link https://valkey.io/commands/lastsave/|valkey.io} for details. * * Command Response - `UNIX TIME` of the last DB save executed with success. */ @@ -3299,9 +3269,8 @@ export class BaseTransaction> { /** * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`. * - * since Valkey version 7.0.0. - * - * See https://valkey.io/commands/lcs/ for more details. + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -3316,9 +3285,8 @@ export class BaseTransaction> { /** * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`. * - * since Valkey version 7.0.0. - * - * See https://valkey.io/commands/lcs/ for more details. + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -3333,9 +3301,8 @@ export class BaseTransaction> { * Returns the indices and lengths of the longest common subsequences between strings stored at * `key1` and `key2`. * - * since Valkey version 7.0.0. - * - * See https://valkey.io/commands/lcs/ for more details. + * @see {@link https://valkey.io/commands/lcs/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. @@ -3362,7 +3329,7 @@ export class BaseTransaction> { /** * Updates the last access time of the specified keys. * - * See https://valkey.io/commands/touch/ for more details. + * @see {@link https://valkey.io/commands/touch/|valkey.io} for details. * * @param keys - The keys to update the last access time of. * @@ -3375,7 +3342,7 @@ export class BaseTransaction> { /** * Returns a random existing key name from the currently selected database. * - * See https://valkey.io/commands/randomkey/ for more details. + * @see {@link https://valkey.io/commands/randomkey/|valkey.io} for details. * * Command Response - A random existing key name from the currently selected database. */ @@ -3388,7 +3355,7 @@ export class BaseTransaction> { * for the entire length of `value`. If the `offset` is larger than the current length of the string at `key`, * the string is padded with zero bytes to make `offset` fit. Creates the `key` if it doesn't exist. * - * See https://valkey.io/commands/setrange/ for more details. + * @see {@link https://valkey.io/commands/setrange/|valkey.io} for details. * * @param key - The key of the string to update. * @param offset - The position in the string where `value` should be written. @@ -3404,7 +3371,7 @@ export class BaseTransaction> { * Appends a `value` to a `key`. If `key` does not exist it is created and set as an empty string, * so `APPEND` will be similar to {@link set} in this special case. * - * See https://valkey.io/commands/append/ for more details. + * @see {@link https://valkey.io/commands/append/|valkey.io} for details. * * @param key - The key of the string. * @param value - The key of the string. @@ -3418,16 +3385,14 @@ export class BaseTransaction> { /** * Pops one or more elements from the first non-empty list from the provided `keys`. * - * See https://valkey.io/commands/lmpop/ for more details. + * @see {@link https://valkey.io/commands/lmpop/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * - * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. * @param keys - An array of keys to lists. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. * @param count - (Optional) The maximum number of popped elements. * * Command Response - A `Record` of `key` name mapped array of popped elements. - * - * since Valkey version 7.0.0. */ public lmpop(keys: string[], direction: ListDirection, count?: number): T { return this.addAndReturn(createLMPop(keys, direction, count)); @@ -3437,7 +3402,8 @@ export class BaseTransaction> { * Blocks the connection until it pops one or more elements from the first non-empty list from the * provided `key`. `BLMPOP` is the blocking variant of {@link lmpop}. * - * See https://valkey.io/commands/blmpop/ for more details. + * @see {@link https://valkey.io/commands/blmpop/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @param keys - An array of keys to lists. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. @@ -3447,8 +3413,6 @@ export class BaseTransaction> { * * Command Response - A `Record` of `key` name mapped array of popped elements. * If no member could be popped and the timeout expired, returns `null`. - * - * since Valkey version 7.0.0. */ public blmpop( keys: string[], @@ -3463,7 +3427,7 @@ export class BaseTransaction> { * Lists the currently active channels. * The command is routed to all nodes, and aggregates the response to a single array. * - * See https://valkey.io/commands/pubsub-channels for more details. + * @see {@link https://valkey.io/commands/pubsub-channels/|valkey.io} for more details. * * @param pattern - A glob-style pattern to match active channels. * If not provided, all active channels are returned. @@ -3481,7 +3445,7 @@ export class BaseTransaction> { * not the count of clients subscribed to patterns. * The command is routed to all nodes, and aggregates the response to the sum of all pattern subscriptions. * - * See https://valkey.io/commands/pubsub-numpat for more details. + * @see {@link https://valkey.io/commands/pubsub-numpat/|valkey.io} for more details. * * Command Response - The number of unique patterns. */ @@ -3495,7 +3459,7 @@ export class BaseTransaction> { * Note that it is valid to call this command without channels. In this case, it will just return an empty map. * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. * - * See https://valkey.io/commands/pubsub-numsub for more details. + * @see {@link https://valkey.io/commands/pubsub-numsub/|valkey.io} for more details. * * @param channels - The list of channels to query for the number of subscribers. * If not provided, returns an empty map. @@ -3530,7 +3494,7 @@ export class Transaction extends BaseTransaction { /// TODO: add MOVE, SLAVEOF and all SENTINEL commands /** Change the currently selected Redis database. - * See https://valkey.io/commands/select/ for details. + * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. * @@ -3548,7 +3512,7 @@ export class Transaction extends BaseTransaction { * * To store the result into a new key, see {@link sortStore}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortOptions}. @@ -3567,7 +3531,7 @@ export class Transaction extends BaseTransaction { * * This command is routed depending on the client's {@link ReadFrom} strategy. * - * since Valkey version 7.0.0. + * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortOptions}. @@ -3587,9 +3551,8 @@ export class Transaction extends BaseTransaction { * * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. * - * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. * @param key - The key of the list, set, or sorted set to be sorted. * @param destination - The key where the sorted result will be stored. * @param options - (Optional) {@link SortOptions}. @@ -3610,7 +3573,8 @@ export class Transaction extends BaseTransaction { * When `replace` is true, removes the `destination` key first if it already exists, otherwise performs * no action. * - * See https://valkey.io/commands/copy/ for more details. + * @see {@link https://valkey.io/commands/copy/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source value. * @param destination - The key where the value should be copied to. @@ -3620,8 +3584,6 @@ export class Transaction extends BaseTransaction { * value to it. If not provided, no action will be performed if the key already exists. * * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. - * - * since Valkey version 6.2.0. */ public copy( source: string, @@ -3634,7 +3596,7 @@ export class Transaction extends BaseTransaction { /** * Move `key` from the currently selected database to the database specified by `dbIndex`. * - * See https://valkey.io/commands/move/ for more details. + * @see {@link https://valkey.io/commands/move/|valkey.io} for details. * * @param key - The key to move. * @param dbIndex - The index of the database to move `key` to. @@ -3648,7 +3610,7 @@ export class Transaction extends BaseTransaction { /** Publish a message on pubsub channel. * - * See https://valkey.io/commands/publish for more details. + * @see {@link https://valkey.io/commands/publish/|valkey.io} for more details. * * @param message - Message to publish. * @param channel - Channel to publish the message on. @@ -3683,7 +3645,7 @@ export class ClusterTransaction extends BaseTransaction { * * To store the result into a new key, see {@link sortStore}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortClusterOptions}. @@ -3702,7 +3664,8 @@ export class ClusterTransaction extends BaseTransaction { * * This command is routed depending on the client's {@link ReadFrom} strategy. * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. + * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. * @param options - (Optional) {@link SortClusterOptions}. @@ -3725,9 +3688,8 @@ export class ClusterTransaction extends BaseTransaction { * * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. * - * See https://valkey.io/commands/sort for more details. + * @see {@link https://valkey.io/commands/sort|valkey.io} for more details. * - * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. * @param key - The key of the list, set, or sorted set to be sorted. * @param destination - The key where the sorted result will be stored. * @param options - (Optional) {@link SortClusterOptions}. @@ -3746,7 +3708,8 @@ export class ClusterTransaction extends BaseTransaction { * Copies the value stored at the `source` to the `destination` key. When `replace` is true, * removes the `destination` key first if it already exists, otherwise performs no action. * - * See https://valkey.io/commands/copy/ for more details. + * @see {@link https://valkey.io/commands/copy/|valkey.io} for details. + * @remarks Since Valkey version 6.2.0. * * @param source - The key to the source value. * @param destination - The key where the value should be copied to. @@ -3754,8 +3717,6 @@ export class ClusterTransaction extends BaseTransaction { * value to it. If not provided, no action will be performed if the key already exists. * * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. - * - * since Valkey version 6.2.0. */ public copy( source: string, @@ -3772,7 +3733,7 @@ export class ClusterTransaction extends BaseTransaction { * 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. + * @see {@link https://valkey.io/commands/publish} and {@link https://valkey.io/commands/spublish} for more details. * * @param message - Message to publish. * @param channel - Channel to publish the message on. @@ -3792,7 +3753,7 @@ export class ClusterTransaction extends BaseTransaction { * Lists the currently active shard channels. * The command is routed to all nodes, and aggregates the response to a single array. * - * See https://valkey.io/commands/pubsub-shardchannels for more details. + * @see {@link https://valkey.io/commands/pubsub-shardchannels|valkey.io} for more details. * * @param pattern - A glob-style pattern to match active shard channels. * If not provided, all active shard channels are returned. @@ -3809,7 +3770,7 @@ export class ClusterTransaction extends BaseTransaction { * Note that it is valid to call this command without channels. In this case, it will just return an empty map. * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. * - * See https://valkey.io/commands/pubsub-shardnumsub for more details. + * @see {@link https://valkey.io/commands/pubsub-shardnumsub|valkey.io} for more details. * * @param channels - The list of shard channels to query for the number of subscribers. * If not provided, returns an empty map. From b25354cfdc151cc57324af63f4568d36017f4083 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:45:59 -0700 Subject: [PATCH 176/236] Node: add SSCAN (#2132) implement SSCAN --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 45 ++++++++++++ node/src/Commands.ts | 17 +++++ node/src/Transaction.ts | 17 +++++ node/tests/SharedTests.ts | 139 +++++++++++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 7 ++ 6 files changed, 225 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5aa121333..ffc5d54672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added SSCAN command ([#2132](https://github.com/valkey-io/valkey-glide/pull/2132)) * Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) * Node: Update all commands to use `async` ([#2110](https://github.com/valkey-io/valkey-glide/pull/2110)) * Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b43e37e50d..4dc45abe4a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -153,6 +153,7 @@ import { createSPop, createSRandMember, createSRem, + createSScan, createSUnion, createSUnionStore, createSet, @@ -2355,6 +2356,50 @@ export class BaseClient { return this.createWritePromise(createSRem(key, members)); } + /** + * Iterates incrementally over a set. + * + * @see {@link https://valkey.io/commands/sscan} for details. + * + * @param key - The key of the set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. + * @param options - The (Optional) {@link BaseScanOptions}. + * @returns An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and for the next iteration of results. + * The `cursor` will be `"0"` on the last iteration of the set. The second element is always an array of the subset of the set held in `key`. + * + * @example + * ```typescript + * // Assume key contains a set with 200 members + * let newCursor = "0"; + * let result = []; + * + * do { + * result = await client.sscan(key1, newCursor, { + * match: "*", + * count: 5, + * }); + * newCursor = result[0]; + * console.log("Cursor: ", newCursor); + * console.log("Members: ", result[1]); + * } while (newCursor !== "0"); + * + * // The output of the code above is something similar to: + * // Cursor: 8, Match: "f*" + * // Members: ['field', 'fur', 'fun', 'fame'] + * // Cursor: 20, Count: 3 + * // Members: ['1', '2', '3', '4', '5', '6'] + * // Cursor: 0 + * // Members: ['1', '2', '3', '4', '5', '6'] + * ``` + */ + public async sscan( + key: string, + cursor: string, + options?: BaseScanOptions, + ): Promise<[string, string[]]> { + return this.createWritePromise(createSScan(key, cursor, options)); + } + /** Returns all the members of the set value stored at `key`. * * @see {@link https://valkey.io/commands/smembers/|valkey.io} for details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 209a896cd8..8a6fd01ae0 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1018,6 +1018,23 @@ export function createSRem( return createCommand(RequestType.SRem, [key].concat(members)); } +/** + * @internal + */ +export function createSScan( + key: string, + cursor: string, + options?: BaseScanOptions, +): command_request.Command { + let args: string[] = [key, cursor]; + + if (options) { + args = args.concat(convertBaseScanOptionsToArgsArray(options)); + } + + return createCommand(RequestType.SScan, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b24d4f8973..c7b7746158 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -186,6 +186,7 @@ import { createSPop, createSRandMember, createSRem, + createSScan, createSUnion, createSUnionStore, createSelect, @@ -1225,6 +1226,22 @@ export class BaseTransaction> { return this.addAndReturn(createSRem(key, members)); } + /** + * Iterates incrementally over a set. + * + * @see {@link https://valkey.io/commands/sscan} for details. + * + * @param key - The key of the set. + * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. + * @param options - The (Optional) {@link BaseScanOptions}. + * + * Command Response - An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and for the next iteration of results. + * The `cursor` will be `"0"` on the last iteration of the set. The second element is always an array of the subset of the set held in `key`. + */ + public sscan(key: string, cursor: string, options?: BaseScanOptions): T { + return this.addAndReturn(createSScan(key, cursor, options)); + } + /** Returns all the members of the set value stored at `key`. * @see {@link https://valkey.io/commands/smembers/|valkey.io} for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d9f3d2129a..1a0e4d2866 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1428,7 +1428,7 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `hscan empty set, negative cursor, negative count, and non-hash key exception tests`, + `hscan and sscan empty set, negative cursor, negative count, and non-hash key exception tests`, async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = "{key}-1" + uuidv4(); @@ -1442,11 +1442,19 @@ export function runBaseTests(config: { expect(result[resultCursorIndex]).toEqual(initialCursor); expect(result[resultCollectionIndex]).toEqual([]); + result = await client.sscan(key1, initialCursor); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + // Negative cursor result = await client.hscan(key1, "-1"); expect(result[resultCursorIndex]).toEqual(initialCursor); expect(result[resultCollectionIndex]).toEqual([]); + result = await client.sscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + // Exceptions // Non-hash key expect(await client.set(key2, "test")).toEqual("OK"); @@ -1460,12 +1468,28 @@ export function runBaseTests(config: { }), ).rejects.toThrow(RequestError); + await expect(client.sscan(key2, initialCursor)).rejects.toThrow( + RequestError, + ); + await expect( + client.sscan(key2, initialCursor, { + match: "test", + count: 30, + }), + ).rejects.toThrow(RequestError); + // Negative count await expect( client.hscan(key2, initialCursor, { count: -1, }), ).rejects.toThrow(RequestError); + + await expect( + client.sscan(key2, initialCursor, { + count: -1, + }), + ).rejects.toThrow(RequestError); }, protocol); }, config.timeout, @@ -2778,6 +2802,119 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sscan test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const initialCursor = "0"; + const defaultCount = 10; + + const numberMembers: string[] = []; + + for (let i = 0; i < 50000; i++) { + numberMembers[i] = i.toString(); + } + + const numberMembersSet: string[] = numberMembers; + const charMembers: string[] = ["a", "b", "c", "d", "e"]; + const charMembersSet: Set = new Set(charMembers); + const resultCursorIndex = 0; + const resultCollectionIndex = 1; + + // Result contains the whole set + expect(await client.sadd(key1, charMembers)).toEqual( + charMembers.length, + ); + let result = await client.sscan(key1, initialCursor); + expect(await result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex].length).toEqual( + charMembers.length, + ); + + const resultMembers = result[resultCollectionIndex] as string[]; + + const allResultMember = resultMembers.every((member) => + charMembersSet.has(member), + ); + expect(allResultMember).toEqual(true); + + // Testing sscan with match + result = await client.sscan(key1, initialCursor, { + match: "a", + }); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual(["a"]); + + // Result contains a subset of the key + expect(await client.sadd(key1, numberMembers)).toEqual( + numberMembers.length, + ); + + let resultCursor = "0"; + let secondResultValues: string[] = []; + + let isFirstLoop = true; + + do { + result = await client.sscan(key1, resultCursor); + resultCursor = result[resultCursorIndex].toString(); + secondResultValues = result[resultCollectionIndex]; + + if (isFirstLoop) { + expect(resultCursor).not.toBe("0"); + isFirstLoop = false; + } else if (resultCursor === initialCursor) { + break; + } + + // Scan with result cursor has a different set + const secondResult = await client.sscan(key1, resultCursor); + const newResultCursor = + secondResult[resultCursorIndex].toString(); + expect(resultCursor).not.toBe(newResultCursor); + resultCursor = newResultCursor; + expect(result[resultCollectionIndex]).not.toBe( + secondResult[resultCollectionIndex], + ); + secondResultValues = secondResult[resultCollectionIndex]; + } while (resultCursor != initialCursor); // 0 is returned for the cursor of the last iteration. + + const allSecondResultValues = Object.keys( + secondResultValues, + ).every((value) => value in numberMembersSet); + expect(allSecondResultValues).toEqual(true); + + // Test match pattern + result = await client.sscan(key1, initialCursor, { + match: "*", + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(defaultCount); + + // Test count + result = await client.sscan(key1, initialCursor, { count: 20 }); + expect(result[resultCursorIndex]).not.toEqual(0); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(20); + + // Test count with match returns a non-empty list + result = await client.sscan(key1, initialCursor, { + match: "1*", + count: 30, + }); + expect(result[resultCursorIndex]).not.toEqual(initialCursor); + expect( + result[resultCollectionIndex].length, + ).toBeGreaterThanOrEqual(0); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sunion test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 7e704667a1..2faf78335d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -881,6 +881,13 @@ export async function transactionTest( responseData.push(["sdiffstore(key7, [key7])", 2]); baseTransaction.srem(key7, ["foo"]); responseData.push(['srem(key7, ["foo"])', 1]); + baseTransaction.sscan(key7, "0"); + responseData.push(['sscan(key7, "0")', ["0", ["bar"]]]); + baseTransaction.sscan(key7, "0", { match: "*", count: 20 }); + responseData.push([ + 'sscan(key7, "0", {match: "*", count: 20})', + ["0", ["bar"]], + ]); baseTransaction.scard(key7); responseData.push(["scard(key7)", 1]); baseTransaction.sismember(key7, "bar"); From a1127a49787b1c45d0e605519447df7595a15b3d Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 15 Aug 2024 11:38:59 -0700 Subject: [PATCH 177/236] CI: fix amazon linux job (#2125) Amazon linux: Install `tar`. Signed-off-by: Yury-Fridlyand --- .github/workflows/install-shared-dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install-shared-dependencies/action.yml b/.github/workflows/install-shared-dependencies/action.yml index af44e7206f..0a134eecc9 100644 --- a/.github/workflows/install-shared-dependencies/action.yml +++ b/.github/workflows/install-shared-dependencies/action.yml @@ -63,7 +63,7 @@ runs: shell: bash if: "${{ inputs.os == 'amazon-linux' }}" run: | - yum install -y gcc pkgconfig openssl openssl-devel which curl gettext --allowerasing + yum install -y gcc pkgconfig openssl openssl-devel which curl gettext tar --allowerasing - name: Install Rust toolchain and protoc if: "${{ !contains(inputs.target, 'musl') }}" From c550a43436c40311da270f31d9348393e494cbcd Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 15 Aug 2024 12:12:22 -0700 Subject: [PATCH 178/236] Java: Add binary support to custom command. (#2109) * Add binary support to custom command. Signed-off-by: Yury-Fridlyand Signed-off-by: Andrew Carbonetto Co-authored-by: Andrew Carbonetto Co-authored-by: Guian Gumpac --- CHANGELOG.md | 1 + .../src/main/java/glide/api/GlideClient.java | 6 ++ .../java/glide/api/GlideClusterClient.java | 26 +++++++ .../api/commands/GenericClusterCommands.java | 58 +++++++++++--- .../glide/api/commands/GenericCommands.java | 27 ++++++- .../glide/api/models/BaseTransaction.java | 2 +- .../java/glide/api/models/ClusterValue.java | 14 ++-- .../test/java/glide/api/GlideClientTest.java | 24 ++++++ .../glide/api/GlideClusterClientTest.java | 75 +++++++++++++++++++ .../glide/api/models/ClusterValueTests.java | 44 ++++++++++- .../test/java/glide/cluster/CommandTests.java | 33 ++++++++ .../java/glide/standalone/CommandTests.java | 8 ++ node/src/GlideClusterClient.ts | 2 +- .../glide/async_commands/cluster_commands.py | 9 ++- 14 files changed, 300 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc5d54672..7fb8977d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Java: Added binary support for custom command ([#2109](https://github.com/valkey-io/valkey-glide/pull/2109)) * Node: Added SSCAN command ([#2132](https://github.com/valkey-io/valkey-glide/pull/2132)) * Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) * Node: Update all commands to use `async` ([#2110](https://github.com/valkey-io/valkey-glide/pull/2110)) diff --git a/java/client/src/main/java/glide/api/GlideClient.java b/java/client/src/main/java/glide/api/GlideClient.java index 53eaeb369d..ee85b4b393 100644 --- a/java/client/src/main/java/glide/api/GlideClient.java +++ b/java/client/src/main/java/glide/api/GlideClient.java @@ -96,6 +96,12 @@ public CompletableFuture customCommand(@NonNull String[] args) { return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectOrNullResponse); } + @Override + public CompletableFuture customCommand(@NonNull GlideString[] args) { + return commandManager.submitNewCommand( + CustomCommand, args, this::handleBinaryObjectOrNullResponse); + } + @Override public CompletableFuture exec(@NonNull Transaction transaction) { if (transaction.isBinaryOutput()) { diff --git a/java/client/src/main/java/glide/api/GlideClusterClient.java b/java/client/src/main/java/glide/api/GlideClusterClient.java index 65b8721ec8..bd01bce6b1 100644 --- a/java/client/src/main/java/glide/api/GlideClusterClient.java +++ b/java/client/src/main/java/glide/api/GlideClusterClient.java @@ -106,6 +106,15 @@ public CompletableFuture> customCommand(@NonNull String[] a CustomCommand, args, response -> ClusterValue.of(handleObjectOrNullResponse(response))); } + @Override + public CompletableFuture> customCommand(@NonNull GlideString[] args) { + // TODO if a command returns a map as a single value, ClusterValue misleads user + return commandManager.submitNewCommand( + CustomCommand, + args, + response -> ClusterValue.of(handleBinaryObjectOrNullResponse(response))); + } + @Override public CompletableFuture> customCommand( @NonNull String[] args, @NonNull Route route) { @@ -113,6 +122,13 @@ public CompletableFuture> customCommand( CustomCommand, args, route, response -> handleCustomCommandResponse(route, response)); } + @Override + public CompletableFuture> customCommand( + @NonNull GlideString[] args, @NonNull Route route) { + return commandManager.submitNewCommand( + CustomCommand, args, route, response -> handleCustomCommandBinaryResponse(route, response)); + } + protected ClusterValue handleCustomCommandResponse(Route route, Response response) { if (route instanceof SingleNodeRoute) { return ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)); @@ -123,6 +139,16 @@ protected ClusterValue handleCustomCommandResponse(Route route, Response return ClusterValue.ofMultiValue(handleMapResponse(response)); } + protected ClusterValue handleCustomCommandBinaryResponse(Route route, Response response) { + if (route instanceof SingleNodeRoute) { + return ClusterValue.ofSingleValue(handleBinaryObjectOrNullResponse(response)); + } + if (response.hasConstantResponse()) { + return ClusterValue.ofSingleValue(handleStringResponse(response)); + } + return ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response)); + } + @Override public CompletableFuture exec(@NonNull ClusterTransaction transaction) { if (transaction.isBinaryOutput()) { diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index 544c40a2c1..dcd0fdd594 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -22,30 +22,45 @@ public interface GenericClusterCommands { /** * Executes a single command, without checking inputs. Every part of the command, including - * subcommands, should be added as a separate value in args. - * - *

The command will be routed to all primaries. + * subcommands, should be added as a separate value in args.
+ * The command will be routed automatically based on the passed command's default request policy. * - * @apiNote See Valkey * GLIDE Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command including the command name. - * @return The returning value depends on the executed command. + * @return The returned value for the custom command. * @example *

{@code
      * ClusterValue data = client.customCommand(new String[] {"ping"}).get();
-     * assert ((String) data.getSingleValue()).equals("PONG");
+     * assert data.getSingleValue().equals("PONG");
      * }
      */
     CompletableFuture> customCommand(String[] args);
 
     /**
      * Executes a single command, without checking inputs. Every part of the command, including
-     * subcommands, should be added as a separate value in args.
+     * subcommands, should be added as a separate value in args.
+ * The command will be routed automatically based on the passed command's default request policy. * - *

Client will route the command to the nodes defined by route. + * @see Valkey + * GLIDE Wiki for details on the restrictions and limitations of the custom command API. + * @param args Arguments for the custom command including the command name. + * @return The returned value for the custom command. + * @example + *

{@code
+     * ClusterValue data = client.customCommand(new GlideString[] {gs("ping")}).get();
+     * assert data.getSingleValue().equals(gs("PONG"));
+     * }
+     */
+    CompletableFuture> customCommand(GlideString[] args);
+
+    /**
+     * Executes a single command, without checking inputs. Every part of the command, including
+     * subcommands, should be added as a separate value in args.
      *
-     * @apiNote See Valkey
      *     GLIDE Wiki for details on the restrictions and limitations of the custom command API.
      * @param args Arguments for the custom command including the command name
@@ -56,12 +71,33 @@ public interface GenericClusterCommands {
      *     
{@code
      * ClusterValue result = clusterClient.customCommand(new String[]{ "CONFIG", "GET", "maxmemory"}, ALL_NODES).get();
      * Map payload = result.getMultiValue();
-     * assert ((String) payload.get("node1")).equals("1GB");
-     * assert ((String) payload.get("node2")).equals("100MB");
+     * assert payload.get("node1").equals("1GB");
+     * assert payload.get("node2").equals("100MB");
      * }
      */
     CompletableFuture> customCommand(String[] args, Route route);
 
+    /**
+     * Executes a single command, without checking inputs. Every part of the command, including
+     * subcommands, should be added as a separate value in args.
+     *
+     * @see Valkey
+     *     GLIDE Wiki for details on the restrictions and limitations of the custom command API.
+     * @param args Arguments for the custom command including the command name
+     * @param route Specifies the routing configuration for the command. The client will route the
+     *     command to the nodes defined by route.
+     * @return The returning value depends on the executed command and route.
+     * @example
+     *     
{@code
+     * ClusterValue result = clusterClient.customCommand(new GlideString[] { gs("CONFIG"), gs("GET"), gs("maxmemory") }, ALL_NODES).get();
+     * Map payload = result.getMultiValue();
+     * assert payload.get(gs("node1")).equals(gs("1GB"));
+     * assert payload.get(gs("node2")).equals(gs("100MB"));
+     * }
+     */
+    CompletableFuture> customCommand(GlideString[] args, Route route);
+
     /**
      * Executes a transaction by processing the queued commands.
      *
diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java
index a40503a37e..ae72938bad 100644
--- a/java/client/src/main/java/glide/api/commands/GenericCommands.java
+++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java
@@ -22,21 +22,40 @@ public interface GenericCommands {
      * Executes a single command, without checking inputs. Every part of the command, including
      * subcommands, should be added as a separate value in args.
      *
-     * @apiNote See Valkey
      *     GLIDE Wiki for details on the restrictions and limitations of the custom command API.
      * @param args Arguments for the custom command.
-     * @return The returning value depends on the executed command.
+     * @return The returned value for the custom command.
      * @example
      *     
{@code
-     * Object response = (String) client.customCommand(new String[] {"ping", "GLIDE"}).get();
-     * assert ((String) response).equals("GLIDE");
+     * Object response = client.customCommand(new String[] {"ping", "GLIDE"}).get();
+     * assert response.equals("GLIDE");
      * // Get a list of all pub/sub clients:
      * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }).get();
      * }
*/ CompletableFuture customCommand(String[] args); + /** + * Executes a single command, without checking inputs. Every part of the command, including + * subcommands, should be added as a separate value in args. + * + * @see Valkey + * GLIDE Wiki for details on the restrictions and limitations of the custom command API. + * @param args Arguments for the custom command. + * @return The returned value for the custom command. + * @example + *
{@code
+     * Object response = client.customCommand(new GlideString[] {gs("ping"), gs("GLIDE")}).get();
+     * assert response.equals(gs("GLIDE"));
+     * // Get a list of all pub/sub clients:
+     * Object result = client.customCommand(new GlideString[] { gs("CLIENT"), gs("LIST"), gs("TYPE"), gs("PUBSUB") }).get();
+     * }
+ */ + CompletableFuture customCommand(GlideString[] args); + /** * Executes a transaction by processing the queued commands. * diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 6766617426..ccf9b956c4 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -339,7 +339,7 @@ public T withBinaryOutput() { * href="https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command">Glide * Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command. - * @return Command Response - A response from the server with an Object. + * @return Command Response - The returned value for the custom command. */ public T customCommand(ArgType[] args) { checkTypeOrThrow(args); diff --git a/java/client/src/main/java/glide/api/models/ClusterValue.java b/java/client/src/main/java/glide/api/models/ClusterValue.java index d141c3cd08..66240a241c 100644 --- a/java/client/src/main/java/glide/api/models/ClusterValue.java +++ b/java/client/src/main/java/glide/api/models/ClusterValue.java @@ -46,13 +46,16 @@ public T getSingleValue() { /** A constructor for the value with type auto-detection. */ @SuppressWarnings("unchecked") public static ClusterValue of(Object data) { - var res = new ClusterValue(); if (data instanceof Map) { - res.multiValue = (Map) data; + var map = (Map) data; + if (map.isEmpty() || map.keySet().toArray()[0] instanceof String) { + return ofMultiValue((Map) data); + } else { // GlideString + return ofMultiValueBinary((Map) data); + } } else { - res.singleValue = (T) data; + return ofSingleValue((T) data); } - return res; } /** A constructor for the value. */ @@ -73,10 +76,9 @@ public static ClusterValue ofMultiValue(Map data) { public static ClusterValue ofMultiValueBinary(Map data) { var res = new ClusterValue(); // the map node address can be converted to a string - Map multiValue = + res.multiValue = data.entrySet().stream() .collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); - res.multiValue = multiValue; return res; } diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index cb29738bbb..9c52ba733a 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -419,6 +419,30 @@ public void customCommand_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void customCommand_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Object value = "testValue"; + GlideString cmd = gs("GETSTRING"); + GlideString[] arguments = new GlideString[] {cmd, key}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(CustomCommand), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.customCommand(arguments); + String payload = (String) response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void exec() { diff --git a/java/client/src/test/java/glide/api/GlideClusterClientTest.java b/java/client/src/test/java/glide/api/GlideClusterClientTest.java index 54b49d9de2..648c64bf33 100644 --- a/java/client/src/test/java/glide/api/GlideClusterClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClusterClientTest.java @@ -120,6 +120,42 @@ public void custom_command_returns_multi_value() { } } + @Test + @SneakyThrows + public void custom_command_binary_returns_single_value() { + var commandManager = new TestCommandManager(null); + + try (var client = new TestClient(commandManager, "TEST")) { + var value = client.customCommand(new GlideString[0]).get(); + assertEquals("TEST", value.getSingleValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_binary_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(new GlideString[0]).get(); + assertEquals(data, value.getMultiValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_binary_returns_multi_binary_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of(gs("key1"), "value1", gs("key2"), "value2"); + var dataNormalized = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(new GlideString[0]).get(); + assertEquals(dataNormalized, value.getMultiValue()); + } + } + @Test @SneakyThrows // test checks that even a map returned as a single value when single node route is used @@ -158,6 +194,45 @@ public void custom_command_returns_single_value_on_constant_response() { } } + @Test + @SneakyThrows + // test checks that even a map returned as a single value when single node route is used + public void custom_command_binary_with_single_node_route_returns_single_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(new GlideString[0], RANDOM).get(); + assertEquals(data, value.getSingleValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_binary_with_multi_node_route_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of(gs("key1"), "value1", gs("key2"), "value2"); + var dataNormalized = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(new GlideString[0], ALL_NODES).get(); + assertEquals(dataNormalized, value.getMultiValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_binary_returns_single_value_on_constant_response() { + var commandManager = + new TestCommandManager( + Response.newBuilder().setConstantResponse(ConstantResponse.OK).build()); + + try (var client = new TestClient(commandManager, "OK")) { + var value = client.customCommand(new GlideString[0], ALL_NODES).get(); + assertEquals("OK", value.getSingleValue()); + } + } + private static class TestClient extends GlideClusterClient { private final Object object; diff --git a/java/client/src/test/java/glide/api/models/ClusterValueTests.java b/java/client/src/test/java/glide/api/models/ClusterValueTests.java index d27bb1aaba..254abd49a4 100644 --- a/java/client/src/test/java/glide/api/models/ClusterValueTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterValueTests.java @@ -41,6 +41,20 @@ public void handle_single_data() { assertThrows(Throwable.class, value::getMultiValue).getMessage())); } + @Test + public void handle_empty_map() { + var value = ClusterValue.of(Map.of()); + assertAll( + () -> assertTrue(value.hasMultiData()), + () -> assertFalse(value.hasSingleData()), + () -> assertNotNull(value.getMultiValue()), + () -> assertEquals(Map.of(), value.getMultiValue()), + () -> + assertEquals( + "No single value stored", + assertThrows(Throwable.class, value::getSingleValue).getMessage())); + } + @Test public void handle_multi_data() { var data = Map.of("node1", Map.of("config1", "param1", "config2", "param2"), "node2", Map.of()); @@ -56,6 +70,32 @@ public void handle_multi_data() { assertThrows(Throwable.class, value::getSingleValue).getMessage())); } + @Test + public void handle_multi_binary_data() { + var data = + Map.of( + gs("node1"), + Map.of(gs("config1"), gs("param1"), gs("config2"), gs("param2")), + gs("node2"), + Map.of()); + var dataNormalized = + Map.of( + "node1", + Map.of(gs("config1"), gs("param1"), gs("config2"), gs("param2")), + "node2", + Map.of()); + var value = ClusterValue.of(data); + assertAll( + () -> assertTrue(value.hasMultiData()), + () -> assertFalse(value.hasSingleData()), + () -> assertNotNull(value.getMultiValue()), + () -> assertEquals(dataNormalized, value.getMultiValue()), + () -> + assertEquals( + "No single value stored", + assertThrows(Throwable.class, value::getSingleValue).getMessage())); + } + @Test public void single_value_ctor() { var value = ClusterValue.ofSingleValue(Map.of("config1", "param1", "config2", "param2")); @@ -87,8 +127,8 @@ public void multi_value_binary_ctor() { () -> assertNotNull(value.getMultiValue()), // ofMultiValueBinary converts the key to a String, but the values are not converted () -> assertTrue(value.getMultiValue().containsKey("config1")), - () -> assertTrue(value.getMultiValue().get("config1").equals(gs("param1"))), + () -> assertEquals(gs("param1"), value.getMultiValue().get("config1")), () -> assertTrue(value.getMultiValue().containsKey("config2")), - () -> assertTrue(value.getMultiValue().get("config2").equals(gs("param2")))); + () -> assertEquals(gs("param2"), value.getMultiValue().get("config2"))); } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 0ea506226e..83c6058d26 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -174,6 +174,17 @@ public void custom_command_info() { } } + @Test + @SneakyThrows + public void custom_command_info_binary() { + ClusterValue data = clusterClient.customCommand(new GlideString[] {gs("info")}).get(); + assertTrue(data.hasMultiData()); + for (Object info : data.getMultiValue().values()) { + assertInstanceOf(GlideString.class, info); + assertTrue(info.toString().contains("# Stats")); + } + } + @Test @SneakyThrows public void custom_command_ping() { @@ -181,6 +192,28 @@ public void custom_command_ping() { assertEquals("PONG", data.getSingleValue()); } + @Test + @SneakyThrows + public void custom_command_ping_binary() { + ClusterValue data = clusterClient.customCommand(new GlideString[] {gs("ping")}).get(); + assertEquals(gs("PONG"), data.getSingleValue()); + } + + @Test + @SneakyThrows + public void custom_command_binary_with_route() { + ClusterValue data = + clusterClient.customCommand(new GlideString[] {gs("info")}, ALL_NODES).get(); + for (Object info : data.getMultiValue().values()) { + assertInstanceOf(GlideString.class, info); + assertTrue(info.toString().contains("# Stats")); + } + + data = clusterClient.customCommand(new GlideString[] {gs("info")}, RANDOM).get(); + assertInstanceOf(GlideString.class, data.getSingleValue()); + assertTrue(data.getSingleValue().toString().contains("# Stats")); + } + @Test @SneakyThrows public void custom_command_del_returns_a_number() { diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index f518766b4c..5cdac40b9c 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -105,6 +105,14 @@ public void custom_command_info() { assertTrue(((String) data).contains("# Stats")); } + @Test + @SneakyThrows + public void custom_command_info_binary() { + Object data = regularClient.customCommand(new GlideString[] {gs("info")}).get(); + assertInstanceOf(GlideString.class, data); + assertTrue(data.toString().contains("# Stats")); + } + @Test @SneakyThrows public void custom_command_del_returns_a_number() { diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 0c3698e170..0aeb430ae4 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -358,7 +358,7 @@ export class GlideClusterClient extends BaseClient { public async customCommand( args: GlideString[], options?: { route?: Routes; decoder?: Decoder }, - ): Promise { + ): Promise> { const command = createCustomCommand(args); return super.createWritePromise(command, { route: toProtobufRoute(options?.route), diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index f59cb4662d..4371d23125 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -31,7 +31,7 @@ class ClusterCommands(CoreCommands): async def custom_command( self, command_args: List[TEncodable], route: Optional[Route] = None - ) -> TResult: + ) -> TClusterResponse[TResult]: """ Executes a single command, without checking inputs. See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) @@ -47,10 +47,11 @@ async def custom_command( case the client will route the command to the nodes defined by `route`. Defaults to None. Returns: - TResult: The returning value depends on the executed command and the route. + TClusterResponse[TResult]: The returning value depends on the executed command and the route. """ - return await self._execute_command( - RequestType.CustomCommand, command_args, route + return cast( + TClusterResponse[TResult], + await self._execute_command(RequestType.CustomCommand, command_args, route), ) async def info( From 91fae38034459b4fd87dabe8866d8f47eaeee7e9 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 15 Aug 2024 12:43:30 -0700 Subject: [PATCH 179/236] Java: add `PUBSUB` `CHANNELS`, `NUMPAT` and `NUMSUB` commands (#2105) Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + .../src/main/java/glide/api/BaseClient.java | 51 ++++ .../api/commands/PubSubBaseCommands.java | 121 +++++++++ .../glide/api/models/BaseTransaction.java | 72 +++++ .../ClusterSubscriptionConfiguration.java | 13 + .../test/java/glide/api/GlideClientTest.java | 167 ++++++++++++ .../glide/api/models/TransactionTests.java | 15 ++ .../src/test/java/glide/PubSubTests.java | 251 +++++++++++++++++- 8 files changed, 679 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb8977d45..12854040b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Java: Added PUBSUB CHANNELS, NUMPAT and NUMSUB commands ([#2105](https://github.com/valkey-io/valkey-glide/pull/2105)) * Java: Added binary support for custom command ([#2109](https://github.com/valkey-io/valkey-glide/pull/2109)) * Node: Added SSCAN command ([#2132](https://github.com/valkey-io/valkey-glide/pull/2132)) * Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 9c6297f0ad..91182e797e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -83,6 +83,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PfAdd; import static command_request.CommandRequestOuterClass.RequestType.PfCount; import static command_request.CommandRequestOuterClass.RequestType.PfMerge; +import static command_request.CommandRequestOuterClass.RequestType.PubSubChannels; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumPat; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumSub; import static command_request.CommandRequestOuterClass.RequestType.Publish; import static command_request.CommandRequestOuterClass.RequestType.RPop; import static command_request.CommandRequestOuterClass.RequestType.RPush; @@ -4495,6 +4498,54 @@ public CompletableFuture publish( }); } + @Override + public CompletableFuture pubsubChannels() { + return commandManager.submitNewCommand( + PubSubChannels, + new String[0], + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture pubsubChannelsBinary() { + return commandManager.submitNewCommand( + PubSubChannels, + new GlideString[0], + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture pubsubChannels(@NonNull String pattern) { + return commandManager.submitNewCommand( + PubSubChannels, + new String[] {pattern}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture pubsubChannels(@NonNull GlideString pattern) { + return commandManager.submitNewCommand( + PubSubChannels, + new GlideString[] {pattern}, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture pubsubNumPat() { + return commandManager.submitNewCommand(PubSubNumPat, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> pubsubNumSub(@NonNull String[] channels) { + return commandManager.submitNewCommand(PubSubNumSub, channels, this::handleMapResponse); + } + + @Override + public CompletableFuture> pubsubNumSub(@NonNull GlideString[] channels) { + return commandManager.submitNewCommand( + PubSubNumSub, channels, this::handleBinaryStringMapResponse); + } + @Override public CompletableFuture watch(@NonNull String[] keys) { return commandManager.submitNewCommand(Watch, keys, this::handleStringResponse); diff --git a/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java b/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java index 6906d98e06..d83038b3b3 100644 --- a/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java @@ -2,6 +2,7 @@ package glide.api.commands; import glide.api.models.GlideString; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -40,4 +41,124 @@ public interface PubSubBaseCommands { * } */ CompletableFuture publish(GlideString message, GlideString channel); + + /** + * Lists the currently active channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @return An Array of all active channels. + * @example + *
{@code
+     * String[] response = client.pubsubChannels().get();
+     * assert Arrays.equals(new String[] { "channel1", "channel2" });
+     * }
+ */ + CompletableFuture pubsubChannels(); + + /** + * Lists the currently active channels.
+ * Unlike of {@link #pubsubChannels()}, returns channel names as {@link GlideString}s. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @return An Array of all active channels. + * @example + *
{@code
+     * GlideString[] response = client.pubsubChannels().get();
+     * assert Arrays.equals(new GlideString[] { "channel1", "channel2" });
+     * }
+ */ + CompletableFuture pubsubChannelsBinary(); + + /** + * Lists the currently active channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @param pattern A glob-style pattern to match active channels. + * @return An Array of currently active channels matching the given pattern. + * @example + *
{@code
+     * String[] response = client.pubsubChannels("news.*").get();
+     * assert Arrays.equals(new String[] { "news.sports", "news.weather" });
+     * }
+ */ + CompletableFuture pubsubChannels(String pattern); + + /** + * Lists the currently active channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @param pattern A glob-style pattern to match active channels. + * @return An Array of currently active channels matching the given pattern. + * @example + *
{@code
+     * GlideString[] response = client.pubsubChannels(gs("news.*")).get();
+     * assert Arrays.equals(new GlideString[] { gs("news.sports"), gs("news.weather") });
+     * }
+ */ + CompletableFuture pubsubChannels(GlideString pattern); + + /** + * Returns the number of unique patterns that are subscribed to by clients. + * + * @apiNote + *
    + *
  • When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + *
  • This is the total number of unique patterns all the clients are subscribed to, not + * the count of clients subscribed to patterns. + *
+ * + * @see valkey.io for details. + * @return The number of unique patterns. + * @example + *
{@code
+     * Long result = client.pubsubNumPat().get();
+     * assert result == 3L;
+     * }
+ */ + CompletableFuture pubsubNumPat(); + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the + * specified channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single map. + * @see valkey.io for details. + * @param channels The list of channels to query for the number of subscribers. + * @return A Map where keys are the channel names and values are the numbers of + * subscribers. + * @example + *
{@code
+     * Map result = client.pubsubNumSub(new String[] {"channel1", "channel2"}).get();
+     * assert result.equals(Map.of("channel1", 3L, "channel2", 5L));
+     * }
+ */ + CompletableFuture> pubsubNumSub(String[] channels); + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the + * specified channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single map. + * @see valkey.io for details. + * @param channels The list of channels to query for the number of subscribers. + * @return A Map where keys are the channel names and values are the numbers of + * subscribers. + * @example + *
{@code
+     * Map result = client.pubsubNumSub(new GlideString[] {gs("channel1"), gs("channel2")}).get();
+     * assert result.equals(Map.of(gs("channel1"), 3L, gs("channel2"), 5L));
+     * }
+ */ + CompletableFuture> pubsubNumSub(GlideString[] channels); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index ccf9b956c4..cf2b77401e 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -105,6 +105,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PfCount; import static command_request.CommandRequestOuterClass.RequestType.PfMerge; import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.PubSubChannels; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumPat; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumSub; import static command_request.CommandRequestOuterClass.RequestType.Publish; import static command_request.CommandRequestOuterClass.RequestType.RPop; import static command_request.CommandRequestOuterClass.RequestType.RPush; @@ -6295,6 +6298,75 @@ public T publish(@NonNull ArgType message, @NonNull ArgType channel) { return getThis(); } + /** + * Lists the currently active channels. + * + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @return Command response - An Array of all active channels. + */ + public T pubsubChannels() { + protobufTransaction.addCommands(buildCommand(PubSubChannels)); + return getThis(); + } + + /** + * Lists the currently active channels. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + * @see valkey.io for details. + * @param pattern A glob-style pattern to match active channels. + * @return Command response - An Array of currently active channels matching the + * given pattern. + */ + public T pubsubChannels(@NonNull ArgType pattern) { + checkTypeOrThrow(pattern); + protobufTransaction.addCommands(buildCommand(PubSubChannels, newArgsBuilder().add(pattern))); + return getThis(); + } + + /** + * Returns the number of unique patterns that are subscribed to by clients. + * + * @apiNote + *
    + *
  • When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single array. + *
  • This is the total number of unique patterns all the clients are subscribed to, not + * the count of clients subscribed to patterns. + *
+ * + * @see valkey.io for details. + * @return Command response - The number of unique patterns. + */ + public T pubsubNumPat() { + protobufTransaction.addCommands(buildCommand(PubSubNumPat)); + return getThis(); + } + + /** + * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the + * specified channels. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote When in cluster mode, the command is routed to all nodes, and aggregates the response + * into a single map. + * @see valkey.io for details. + * @param channels The list of channels to query for the number of subscribers. + * @return Command response - A Map where keys are the channel names and values are + * the numbers of subscribers. + */ + public T pubsubNumSub(@NonNull ArgType[] channels) { + checkTypeOrThrow(channels); + protobufTransaction.addCommands(buildCommand(PubSubNumSub, newArgsBuilder().add(channels))); + return getThis(); + } + /** * Gets the union of all the given sets. * diff --git a/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java index a29ddd3d83..c45d6abb33 100644 --- a/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java @@ -1,6 +1,8 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; +import static glide.api.models.GlideString.gs; + import glide.api.GlideClusterClient; import glide.api.models.GlideString; import java.util.HashMap; @@ -91,6 +93,17 @@ public ClusterSubscriptionConfigurationBuilder subscription( return this; } + /** + * Add a subscription to a channel or to multiple channels if {@link + * PubSubClusterChannelMode#PATTERN} is used.
+ * See {@link ClusterSubscriptionConfiguration#subscriptions}. + */ + public ClusterSubscriptionConfigurationBuilder subscription( + PubSubClusterChannelMode mode, String channelOrPattern) { + addSubscription(subscriptions, mode, gs(channelOrPattern)); + return this; + } + /** * Set all subscriptions in a bulk. Rewrites previously stored configurations.
* See {@link ClusterSubscriptionConfiguration#subscriptions}. diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 9c52ba733a..50e812a511 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -107,6 +107,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PfCount; import static command_request.CommandRequestOuterClass.RequestType.PfMerge; import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.PubSubChannels; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumPat; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumSub; import static command_request.CommandRequestOuterClass.RequestType.Publish; import static command_request.CommandRequestOuterClass.RequestType.RPop; import static command_request.CommandRequestOuterClass.RequestType.RPush; @@ -13518,6 +13521,170 @@ public void publish_returns_success() { assertEquals(OK, payload); } + @SneakyThrows + @Test + public void pubsubChannels_returns_success() { + // setup + String[] arguments = new String[0]; + String[] value = new String[] {"ch1", "ch2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PubSubChannels), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pubsubChannels(); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubChannelsBinary_returns_success() { + // setup + GlideString[] arguments = new GlideString[0]; + GlideString[] value = new GlideString[] {gs("ch1"), gs("ch2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PubSubChannels), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pubsubChannelsBinary(); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubChannels_with_pattern_returns_success() { + // setup + String pattern = "ch*"; + String[] arguments = new String[] {pattern}; + String[] value = new String[] {"ch1", "ch2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PubSubChannels), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pubsubChannels(pattern); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubChannelsBinary_with_pattern_returns_success() { + // setup + GlideString pattern = gs("ch*"); + GlideString[] arguments = new GlideString[] {pattern}; + GlideString[] value = new GlideString[] {gs("ch1"), gs("ch2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PubSubChannels), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pubsubChannels(pattern); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubNumPat_returns_success() { + // setup + String[] arguments = new String[0]; + Long value = 42L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PubSubNumPat), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pubsubNumPat(); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubNumSub_returns_success() { + // setup + String[] arguments = new String[] {"ch1", "ch2"}; + Map value = Map.of(); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(PubSubNumSub), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.pubsubNumSub(arguments); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pubsubNumSubBinary_returns_success() { + // setup + GlideString[] arguments = new GlideString[] {gs("ch1"), gs("ch2")}; + Map value = Map.of(); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(PubSubNumSub), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.pubsubNumSub(arguments); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void sunion_returns_success() { diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index aa49a6849b..c112ff7599 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -104,6 +104,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PfCount; import static command_request.CommandRequestOuterClass.RequestType.PfMerge; import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.PubSubChannels; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumPat; +import static command_request.CommandRequestOuterClass.RequestType.PubSubNumSub; import static command_request.CommandRequestOuterClass.RequestType.Publish; import static command_request.CommandRequestOuterClass.RequestType.RPop; import static command_request.CommandRequestOuterClass.RequestType.RPush; @@ -1278,6 +1281,18 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.publish("msg", "ch1"); results.add(Pair.of(Publish, buildArgs("ch1", "msg"))); + transaction.pubsubChannels(); + results.add(Pair.of(PubSubChannels, buildArgs())); + + transaction.pubsubChannels("pattern"); + results.add(Pair.of(PubSubChannels, buildArgs("pattern"))); + + transaction.pubsubNumPat(); + results.add(Pair.of(PubSubNumPat, buildArgs())); + + transaction.pubsubNumSub(new String[] {"ch1", "ch2"}); + results.add(Pair.of(PubSubNumSub, buildArgs("ch1", "ch2"))); + transaction.lcsIdx("key1", "key2"); results.add(Pair.of(LCS, buildArgs("key1", "key2", IDX_COMMAND_STRING))); diff --git a/java/integTest/src/test/java/glide/PubSubTests.java b/java/integTest/src/test/java/glide/PubSubTests.java index 6ca9b3691f..19166cbbf0 100644 --- a/java/integTest/src/test/java/glide/PubSubTests.java +++ b/java/integTest/src/test/java/glide/PubSubTests.java @@ -2,6 +2,7 @@ package glide; import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClientConfig; import static glide.TestUtilities.commonClusterClientConfig; import static glide.api.BaseClient.OK; @@ -27,6 +28,8 @@ import glide.api.models.configuration.BaseSubscriptionConfiguration.MessageCallback; import glide.api.models.configuration.ClusterSubscriptionConfiguration; import glide.api.models.configuration.ClusterSubscriptionConfiguration.PubSubClusterChannelMode; +import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; +import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; import glide.api.models.configuration.StandaloneSubscriptionConfiguration; import glide.api.models.configuration.StandaloneSubscriptionConfiguration.PubSubChannelMode; import glide.api.models.exceptions.ConfigurationError; @@ -49,8 +52,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -111,10 +115,9 @@ private BaseClient createClientWithSubscriptions( @SneakyThrows private BaseClient createClient(boolean standalone) { - if (standalone) { - return GlideClient.createClient(commonClientConfig().build()).get(); - } - return GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); + return standalone + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); } /** @@ -128,17 +131,23 @@ private BaseClient createClient(boolean standalone) { private static final int MESSAGE_DELIVERY_DELAY = 500; // ms - @BeforeEach + @AfterEach @SneakyThrows public void cleanup() { for (var client : clients) { if (client instanceof GlideClusterClient) { - ((GlideClusterClient) client).customCommand(new String[] {"unsubscribe"}, ALL_NODES).get(); - ((GlideClusterClient) client).customCommand(new String[] {"punsubscribe"}, ALL_NODES).get(); - ((GlideClusterClient) client).customCommand(new String[] {"sunsubscribe"}, ALL_NODES).get(); + ((GlideClusterClient) client) + .customCommand(new GlideString[] {gs("unsubscribe")}, ALL_NODES) + .get(); + ((GlideClusterClient) client) + .customCommand(new GlideString[] {gs("punsubscribe")}, ALL_NODES) + .get(); + ((GlideClusterClient) client) + .customCommand(new GlideString[] {gs("sunsubscribe")}, ALL_NODES) + .get(); } else { - ((GlideClient) client).customCommand(new String[] {"unsubscribe"}).get(); - ((GlideClient) client).customCommand(new String[] {"punsubscribe"}).get(); + ((GlideClient) client).customCommand(new GlideString[] {gs("unsubscribe")}).get(); + ((GlideClient) client).customCommand(new GlideString[] {gs("punsubscribe")}).get(); } client.close(); } @@ -1232,7 +1241,7 @@ public void pubsub_with_binary(boolean standalone) { createClientWithSubscriptions( standalone, subscriptions, Optional.of(callback), Optional.of(callbackMessages)); var sender = createClient(standalone); - clients.addAll(Arrays.asList(listener, listener2, sender)); + clients.addAll(List.of(listener, listener2, sender)); assertEquals(OK, sender.publish(message.getMessage(), channel).get()); Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages @@ -1241,4 +1250,222 @@ public void pubsub_with_binary(boolean standalone) { assertEquals(1, callbackMessages.size()); assertEquals(message, callbackMessages.get(0)); } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_channels(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + // no channels exists yet + var client = createClient(standalone); + assertEquals(0, client.pubsubChannels().get().length); + assertEquals(0, client.pubsubChannelsBinary().get().length); + assertEquals(0, client.pubsubChannels("**").get().length); + assertEquals(0, client.pubsubChannels(gs("**")).get().length); + + var channels = Set.of("test_channel1", "test_channel2", "some_channel"); + String pattern = "test_*"; + + Map> subscriptions = + standalone + ? Map.of( + PubSubChannelMode.EXACT, + channels.stream().map(GlideString::gs).collect(Collectors.toSet())) + : Map.of( + PubSubClusterChannelMode.EXACT, + channels.stream().map(GlideString::gs).collect(Collectors.toSet())); + + var listener = createClientWithSubscriptions(standalone, subscriptions); + clients.addAll(List.of(client, listener)); + + // test without pattern + assertEquals(channels, Set.of(client.pubsubChannels().get())); + assertEquals(channels, Set.of(listener.pubsubChannels().get())); + assertEquals( + channels.stream().map(GlideString::gs).collect(Collectors.toSet()), + Set.of(client.pubsubChannelsBinary().get())); + assertEquals( + channels.stream().map(GlideString::gs).collect(Collectors.toSet()), + Set.of(listener.pubsubChannelsBinary().get())); + + // test with pattern + assertEquals( + Set.of("test_channel1", "test_channel2"), Set.of(client.pubsubChannels(pattern).get())); + assertEquals( + Set.of(gs("test_channel1"), gs("test_channel2")), + Set.of(client.pubsubChannels(gs(pattern)).get())); + assertEquals( + Set.of("test_channel1", "test_channel2"), Set.of(listener.pubsubChannels(pattern).get())); + assertEquals( + Set.of(gs("test_channel1"), gs("test_channel2")), + Set.of(listener.pubsubChannels(gs(pattern)).get())); + + // test with non-matching pattern + assertEquals(0, client.pubsubChannels("non_matching_*").get().length); + assertEquals(0, client.pubsubChannels(gs("non_matching_*")).get().length); + assertEquals(0, listener.pubsubChannels("non_matching_*").get().length); + assertEquals(0, listener.pubsubChannels(gs("non_matching_*")).get().length); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_numpat(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + // no channels exists yet + var client = createClient(standalone); + assertEquals(0, client.pubsubNumPat().get()); + + var patterns = Set.of("news.*", "announcements.*"); + + Map> subscriptions = + standalone + ? Map.of( + PubSubChannelMode.PATTERN, + patterns.stream().map(GlideString::gs).collect(Collectors.toSet())) + : Map.of( + PubSubClusterChannelMode.PATTERN, + patterns.stream().map(GlideString::gs).collect(Collectors.toSet())); + + var listener = createClientWithSubscriptions(standalone, subscriptions); + clients.addAll(List.of(client, listener)); + + assertEquals(2, client.pubsubNumPat().get()); + assertEquals(2, listener.pubsubNumPat().get()); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_numsub(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + // no channels exists yet + var client = createClient(standalone); + var channels = new String[] {"channel1", "channel2", "channel3"}; + assertEquals( + Arrays.stream(channels).collect(Collectors.toMap(c -> c, c -> 0L)), + client.pubsubNumSub(channels).get()); + + Map> subscriptions1 = + standalone + ? Map.of( + PubSubChannelMode.EXACT, Set.of(gs("channel1"), gs("channel2"), gs("channel3"))) + : Map.of( + PubSubClusterChannelMode.EXACT, + Set.of(gs("channel1"), gs("channel2"), gs("channel3"))); + var listener1 = createClientWithSubscriptions(standalone, subscriptions1); + + Map> subscriptions2 = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(gs("channel2"), gs("channel3"))) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(gs("channel2"), gs("channel3"))); + var listener2 = createClientWithSubscriptions(standalone, subscriptions2); + + Map> subscriptions3 = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(gs("channel3"))) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(gs("channel3"))); + var listener3 = createClientWithSubscriptions(standalone, subscriptions3); + + Map> subscriptions4 = + standalone + ? Map.of(PubSubChannelMode.PATTERN, Set.of(gs("channel*"))) + : Map.of(PubSubClusterChannelMode.PATTERN, Set.of(gs("channel*"))); + var listener4 = createClientWithSubscriptions(standalone, subscriptions4); + + clients.addAll(List.of(client, listener1, listener2, listener3, listener4)); + + var expected = Map.of("channel1", 1L, "channel2", 2L, "channel3", 3L, "channel4", 0L); + assertEquals(expected, client.pubsubNumSub(ArrayUtils.addFirst(channels, "channel4")).get()); + assertEquals(expected, listener1.pubsubNumSub(ArrayUtils.addFirst(channels, "channel4")).get()); + + var expectedGs = + Map.of(gs("channel1"), 1L, gs("channel2"), 2L, gs("channel3"), 3L, gs("channel4"), 0L); + assertEquals( + expectedGs, + client + .pubsubNumSub( + new GlideString[] {gs("channel1"), gs("channel2"), gs("channel3"), gs("channel4")}) + .get()); + assertEquals( + expectedGs, + listener2 + .pubsubNumSub( + new GlideString[] {gs("channel1"), gs("channel2"), gs("channel3"), gs("channel4")}) + .get()); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_channels_and_numpat_and_numsub_in_transaction(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + var prefix = "{boo}-"; + var route = new SlotKeyRoute(prefix, SlotType.PRIMARY); + var client = createClient(standalone); + var channels = + new String[] {prefix + "test_channel1", prefix + "test_channel2", prefix + "some_channel"}; + var patterns = Set.of(prefix + "news.*", prefix + "announcements.*"); + String pattern = prefix + "test_*"; + + var transaction = + (standalone ? new Transaction() : new ClusterTransaction()) + .pubsubChannels() + .pubsubChannels(pattern) + .pubsubNumPat() + .pubsubNumSub(channels); + // no channels exists yet + var result = + standalone + ? ((GlideClient) client).exec((Transaction) transaction).get() + : ((GlideClusterClient) client).exec((ClusterTransaction) transaction, route).get(); + assertDeepEquals( + new Object[] { + new String[0], // pubsubChannels() + new String[0], // pubsubChannels(pattern) + 0L, // pubsubNumPat() + Arrays.stream(channels) + .collect(Collectors.toMap(c -> c, c -> 0L)), // pubsubNumSub(channels) + }, + result); + + Map> subscriptions = + standalone + ? Map.of( + PubSubChannelMode.EXACT, + Arrays.stream(channels).map(GlideString::gs).collect(Collectors.toSet()), + PubSubChannelMode.PATTERN, + patterns.stream().map(GlideString::gs).collect(Collectors.toSet())) + : Map.of( + PubSubClusterChannelMode.EXACT, + Arrays.stream(channels).map(GlideString::gs).collect(Collectors.toSet()), + PubSubClusterChannelMode.PATTERN, + patterns.stream().map(GlideString::gs).collect(Collectors.toSet())); + + var listener = createClientWithSubscriptions(standalone, subscriptions); + clients.addAll(List.of(client, listener)); + + result = + standalone + ? ((GlideClient) client).exec((Transaction) transaction).get() + : ((GlideClusterClient) client).exec((ClusterTransaction) transaction, route).get(); + + // convert arrays to sets, because we can't compare arrays - they received reordered + result[0] = Set.of((Object[]) result[0]); + result[1] = Set.of((Object[]) result[1]); + + assertDeepEquals( + new Object[] { + Set.of(channels), // pubsubChannels() + Set.of("{boo}-test_channel1", "{boo}-test_channel2"), // pubsubChannels(pattern) + 2L, // pubsubNumPat() + Arrays.stream(channels) + .collect(Collectors.toMap(c -> c, c -> 1L)), // pubsubNumSub(channels) + }, + result); + } } From 6335548dbaf011883104b7c437babcf74f124a0b Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 15 Aug 2024 13:49:38 -0700 Subject: [PATCH 180/236] Java & Node: Update docs for `LCS`. (#2087) Update docs. Signed-off-by: Yury-Fridlyand --- .../api/commands/StringBaseCommands.java | 452 +++++++++--------- .../glide/api/models/BaseTransaction.java | 165 ++----- node/src/BaseClient.ts | 6 +- node/src/Transaction.ts | 2 + 4 files changed, 276 insertions(+), 349 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 38f6c50117..3f46f6a2cb 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -672,8 +672,8 @@ public interface StringBaseCommands { CompletableFuture append(GlideString key, GlideString value); /** - * Returns the longest common subsequence between strings stored at key1 and - * key2. + * Returns all the longest common subsequences combined between strings stored at key1 + * and key2. * * @since Valkey 7.0 and above. * @apiNote When in cluster mode, key1 and key2 must map to the same @@ -681,9 +681,9 @@ public interface StringBaseCommands { * @see valkey.io for details. * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. - * @return A String containing the longest common subsequence between the 2 strings. - * An empty String is returned if the keys do not exist or have no common - * subsequences. + * @return A String containing all the longest common subsequences combined between + * the 2 strings. An empty String is returned if the keys do not exist or have no + * common subsequences. * @example *
{@code
      * // testKey1 = abcd, testKey2 = axcd
@@ -694,8 +694,8 @@ public interface StringBaseCommands {
     CompletableFuture lcs(String key1, String key2);
 
     /**
-     * Returns the longest common subsequence between strings stored at key1 and 
-     * key2.
+     * Returns all the longest common subsequences combined between strings stored at key1
+     *  and key2.
      *
      * @since Valkey 7.0 and above.
      * @apiNote When in cluster mode, key1 and key2 must map to the same
@@ -703,9 +703,9 @@ public interface StringBaseCommands {
      * @see valkey.io for details.
      * @param key1 The key that stores the first string.
      * @param key2 The key that stores the second string.
-     * @return A String containing the longest common subsequence between the 2 strings.
-     *     An empty String is returned if the keys do not exist or have no common
-     *     subsequences.
+     * @return A String containing all the longest common subsequences combined between
+     *     the 2 strings. An empty GlideString is returned if the keys do not exist or
+     *     have no common subsequences.
      * @example
      *     
{@code
      * // testKey1 = abcd, testKey2 = axcd
@@ -716,8 +716,8 @@ public interface StringBaseCommands {
     CompletableFuture lcs(GlideString key1, GlideString key2);
 
     /**
-     * Returns the length of the longest common subsequence between strings stored at key1
-     *  and key2.
+     * Returns the total length of all the longest common subsequences between strings stored at
+     * key1 and key2.
      *
      * @since Valkey 7.0 and above.
      * @apiNote When in cluster mode, key1 and key2 must map to the same
@@ -725,7 +725,7 @@ public interface StringBaseCommands {
      * @see valkey.io for details.
      * @param key1 The key that stores the first string.
      * @param key2 The key that stores the second string.
-     * @return The length of the longest common subsequence between the 2 strings.
+     * @return The total length of all the longest common subsequences the 2 strings.
      * @example
      *     
{@code
      * // testKey1 = abcd, testKey2 = axcd
@@ -736,8 +736,8 @@ public interface StringBaseCommands {
     CompletableFuture lcsLen(String key1, String key2);
 
     /**
-     * Returns the length of the longest common subsequence between strings stored at key1
-     *  and key2.
+     * Returns the total length of all the longest common subsequences between strings stored at
+     * key1 and key2.
      *
      * @since Valkey 7.0 and above.
      * @apiNote When in cluster mode, key1 and key2 must map to the same
@@ -756,8 +756,8 @@ public interface StringBaseCommands {
     CompletableFuture lcsLen(GlideString key1, GlideString key2);
 
     /**
-     * Returns the indices and length of the longest common subsequence between strings stored at
-     * key1 and key2.
+     * Returns the indices and the total length of all the longest common subsequences between strings
+     * stored at key1 and key2.
      *
      * @since Valkey 7.0 and above.
      * @apiNote When in cluster mode, key1 and key2 must map to the same
@@ -766,41 +766,41 @@ public interface StringBaseCommands {
      * @param key1 The key that stores the first string.
      * @param key2 The key that stores the second string.
      * @return A Map containing the indices of the longest common subsequence between the
-     *     2 strings and the length of the longest common subsequence. The resulting map contains two
-     *     keys, "matches" and "len":
+     *     2 strings and the total length of all the longest common subsequences. The resulting map
+     *     contains two keys, "matches" and "len":
      *     
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the string "abcd123" and key2 - * holds the string "bcdef123" then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Long[][][] {
-     *      {
-     *          {4L, 6L},
-     *          {5L, 7L}
-     *      },
-     *      {
-     *          {1L, 3L},
-     *          {0L, 2L}
-     *      }
-     *  }
+     * client.mset(Map.of("key1", "abcd123", "key2", "bcdef123")).get();
+     * Map response = client.lcsIdx("key1", "key2").get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Long[][][] {
+     *         {                         // the first substring match is `"123"`
+     *             {4L, 6L},             // in `"key1"` it is located between indices `4` and `6`
+     *             {5L, 7L}              // and in `"key2"` - in between `5` and `7`
+     *         },
+     *         {                         // second substring match is `"bcd"`
+     *             {1L, 3L},             // in `"key1"` it is located between indices `1` and `3`
+     *             {0L, 2L}              // and in `"key2"` - in between `0` and `2`
+     *         }
+     *     },
+     *     "len", 6                      // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is "123" in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * "bcd" in key1 at index 1 to 3 which matches - * the substring in key2 at index 0 to 2. */ CompletableFuture> lcsIdx(String key1, String key2); /** - * Returns the indices and length of the longest common subsequence between strings stored at - * key1 and key2. + * Returns the indices and the total length of all the longest common subsequences between strings + * stored at key1 and key2. * * @since Valkey 7.0 and above. * @apiNote When in cluster mode, key1 and key2 must map to the same @@ -809,41 +809,41 @@ public interface StringBaseCommands { * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the total length of all the longest common subsequences. The resulting map + * contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the GlideString gs("abcd123") and key2 - * holds the GlideString gs("bcdef123") then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Long[][][] {
-     *      {
-     *          {4L, 6L},
-     *          {5L, 7L}
-     *      },
-     *      {
-     *          {1L, 3L},
-     *          {0L, 2L}
-     *      }
-     *  }
+     * client.mset(Map.of(gs("key1"), gs("abcd123"), gs("key2"), gs("bcdef123"))).get();
+     * Map response = client.lcsIdx(gs("key1"), gs("key2")).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Long[][][] {
+     *         {                         // the first substring match is `gs("123")`
+     *             {4L, 6L},             // in `gs("key1")` it is located between indices `4` and `6`
+     *             {5L, 7L}              // and in `gs("key2")` - in between `5` and `7`
+     *         },
+     *         {                         // second substring match is `gs("bcd")`
+     *             {1L, 3L},             // in `gs("key1")` it is located between indices `1` and `3`
+     *             {0L, 2L}              // and in `gs("key2")` - in between `0` and `2`
+     *         }
+     *     },
+     *     "len", 6                      // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is gs("123") in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * gs("bcd") in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2. */ CompletableFuture> lcsIdx(GlideString key1, GlideString key2); /** - * Returns the indices and length of the longest common subsequence between strings stored at - * key1 and key2. + * Returns the indices and the total length of all the longest common subsequences between strings + * stored at key1 and key2. * * @since Valkey 7.0 and above. * @apiNote When in cluster mode, key1 and key2 must map to the same @@ -853,41 +853,42 @@ public interface StringBaseCommands { * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the total length of all the longest common subsequences. The resulting map + * contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. This value doesn't count towards the + * minMatchLen filter. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the string "abcd123" and key2 - * holds the string "bcdef123" then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Long[][][] {
-     *      {
-     *          {4L, 6L},
-     *          {5L, 7L}
-     *      },
-     *      {
-     *          {1L, 3L},
-     *          {0L, 2L}
-     *      }
-     *  }
+     * client.mset(Map.of("key1", "abcd123", "key2", "bcdef123")).get();
+     * Map response = client.lcsIdx("key1", "key2", 2).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Long[][][] {
+     *         {                         // the first substring match is `"123"`
+     *             {4L, 6L},             // in `"key1"` it is located between indices `4` and `6`
+     *             {5L, 7L}              // and in `"key2"` - in between `5` and `7`
+     *         },
+     *         {                         // second substring match is `"bcd"`
+     *             {1L, 3L},             // in `"key1"` it is located between indices `1` and `3`
+     *             {0L, 2L}              // and in `"key2"` - in between `0` and `2`
+     *         }
+     *     },
+     *     "len", 6                      // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is "123" in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * "bcd" in key1 at index 1 to 3 which matches - * the substring in key2 at index 0 to 2. */ CompletableFuture> lcsIdx(String key1, String key2, long minMatchLen); /** - * Returns the indices and length of the longest common subsequence between strings stored at - * key1 and key2. + * Returns the indices and the total length of all the longest common subsequences between strings + * stored at key1 and key2. * * @since Valkey 7.0 and above. * @apiNote When in cluster mode, key1 and key2 must map to the same @@ -897,41 +898,42 @@ public interface StringBaseCommands { * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the total length of all the longest common subsequences. The resulting map + * contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. This value doesn't count towards the + * minMatchLen filter. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the GlideString gs("abcd123") and key2 - * holds the GlideString gs("bcdef123") then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Long[][][] {
-     *      {
-     *          {4L, 6L},
-     *          {5L, 7L}
-     *      },
-     *      {
-     *          {1L, 3L},
-     *          {0L, 2L}
-     *      }
-     *  }
+     * client.mset(Map.of(gs("key1"), gs("abcd123"), gs("key2"), gs("bcdef123"))).get();
+     * Map response = client.lcsIdx(gs("key1"), gs("key2"), 2).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Long[][][] {
+     *         {                         // the first substring match is `gs("123")`
+     *             {4L, 6L},             // in `gs("key1")` it is located between indices `4` and `6`
+     *             {5L, 7L}              // and in `gs("key2")` - in between `5` and `7`
+     *         },
+     *         {                         // second substring match is `gs("bcd")`
+     *             {1L, 3L},             // in `gs("key1")` it is located between indices `1` and `3`
+     *             {0L, 2L}              // and in `gs("key2")` - in between `0` and `2`
+     *         }
+     *     },
+     *     "len", 6                      // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is gs("123") in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * gs("bcd") in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2. */ CompletableFuture> lcsIdx( GlideString key1, GlideString key2, long minMatchLen); /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -941,42 +943,42 @@ CompletableFuture> lcsIdx( * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the lengths of the longest common subsequences. The resulting map contains + * two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the string "abcd1234" and key2 - * holds the string "bcdef1234" then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Object[] {
-     *      new Object[] {
-     *          new Long[] {4L, 7L},
-     *          new Long[] {5L, 8L},
-     *          4L},
-     *      new Object[] {
-     *          new Long[] {1L, 3L},
-     *          new Long[] {0L, 2L},
-     *          3L}
-     *      }
+     * client.mset(Map.of("key1", "abcd1234", "key2", "bcdef1234")).get();
+     * Map response = client.lcsIdxWithMatchLen("key1", "key2").get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Object[][] {
+     *         {                                    // the first substring match is `"1234"`
+     *             new Long[] {4L, 7L},             // in `"key1"` it is located between indices `4` and `7`
+     *             new Long[] {5L, 8L},             // and in `"key2"` - in between `5` and `8`
+     *             4L                               // the match length
+     *         },
+     *         {                                    // second substring match is `"bcd"`
+     *             new Long[] {1L, 3L},             // in `"key1"` it is located between indices `1` and `3`
+     *             new Long[] {0L, 2L},             // and in `"key2"` - in between `0` and `2`
+     *             3L                               // the match length
+     *         }
+     *     },
+     *     "len", 6                                 // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is "1234" in key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * "bcd" in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. */ CompletableFuture> lcsIdxWithMatchLen(String key1, String key2); /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -986,43 +988,42 @@ CompletableFuture> lcsIdx( * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the lengths of the longest common subsequences. The resulting map contains + * two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the GlideString gs("abcd1234") and key2 - * holds the GlideString gs("bcdef1234") then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Object[] {
-     *      new Object[] {
-     *          new Long[] {4L, 7L},
-     *          new Long[] {5L, 8L},
-     *          4L},
-     *      new Object[] {
-     *          new Long[] {1L, 3L},
-     *          new Long[] {0L, 2L},
-     *          3L}
-     *      }
+     * client.mset(Map.of(gs("key1"), gs("abcd1234"), gs("key2"), gs("bcdef1234"))).get();
+     * Map response = client.lcsIdxWithMatchLen(gs("key1"), gs("key2")).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Object[][] {
+     *         {                                    // the first substring match is `gs("1234")`
+     *             new Long[] {4L, 7L},             // in `gs("key1")` it is located between indices `4` and `7`
+     *             new Long[] {5L, 8L},             // and in `gs("key2")` - in between `5` and `8`
+     *             4L                               // the match length
+     *         },
+     *         {                                    // second substring match is `"bcd"`
+     *             new Long[] {1L, 3L},             // in `gs("key1")` it is located between indices `1` and `3`
+     *             new Long[] {0L, 2L},             // and in `gs("key2")` - in between `0` and `2`
+     *             3L                               // the match length
+     *         }
+     *     },
+     *     "len", 6                                 // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is gs("1234") in - * key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * gs("bcd") in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. */ CompletableFuture> lcsIdxWithMatchLen(GlideString key1, GlideString key2); /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -1033,43 +1034,44 @@ CompletableFuture> lcsIdx( * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the total length of all the longest common subsequences. The resulting map + * contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. This value doesn't count towards the + * minMatchLen filter. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the string "abcd1234" and key2 - * holds the string "bcdef1234" then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Object[] {
-     *      new Object[] {
-     *          new Long[] {4L, 7L},
-     *          new Long[] {5L, 8L},
-     *          4L},
-     *      new Object[] {
-     *          new Long[] {1L, 3L},
-     *          new Long[] {0L, 2L},
-     *          3L}
-     *      }
+     * client.mset(Map.of("key1", "abcd1234", "key2", "bcdef1234")).get();
+     * Map response = client.lcsIdxWithMatchLen("key1", "key2", 2).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Object[][] {
+     *         {                                    // the first substring match is `"1234"`
+     *             new Long[] {4L, 7L},             // in `"key1"` it is located between indices `4` and `7`
+     *             new Long[] {5L, 8L},             // and in `"key2"` - in between `5` and `8`
+     *             4L                               // the match length
+     *         },
+     *         {                                    // second substring match is `"bcd"`
+     *             new Long[] {1L, 3L},             // in `"key1"` it is located between indices `1` and `3`
+     *             new Long[] {0L, 2L},             // and in `"key2"` - in between `0` and `2`
+     *             3L                               // the match length
+     *         }
+     *     },
+     *     "len", 6                                 // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is "1234" in key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * "bcd" in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. */ CompletableFuture> lcsIdxWithMatchLen( String key1, String key2, long minMatchLen); /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -1080,38 +1082,38 @@ CompletableFuture> lcsIdxWithMatchLen( * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return A Map containing the indices of the longest common subsequence between the - * 2 strings and the length of the longest common subsequence. The resulting map contains two - * keys, "matches" and "len": + * 2 strings and the total length of all the longest common subsequences. The resulting map + * contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. This value doesn't count towards the + * minMatchLen filter. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the GlideString gs("abcd1234") and key2 - * holds the GlideString gs("bcdef1234") then the sample result would be + * See example for more details. + * @example *
{@code
-     * new Object[] {
-     *      new Object[] {
-     *          new Long[] {4L, 7L},
-     *          new Long[] {5L, 8L},
-     *          4L},
-     *      new Object[] {
-     *          new Long[] {1L, 3L},
-     *          new Long[] {0L, 2L},
-     *          3L}
-     *      }
+     * client.mset(Map.of(gs("key1"), gs("abcd1234"), gs("key2"), gs("bcdef1234"))).get();
+     * Map response = client.lcsIdxWithMatchLen(gs("key1"), gs("key2"), 2).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "matches", new Object[][] {
+     *         {                                    // the first substring match is `gs("1234")`
+     *             new Long[] {4L, 7L},             // in `gs("key1")` it is located between indices `4` and `7`
+     *             new Long[] {5L, 8L},             // and in `gs("key2")` - in between `5` and `8`
+     *             4L                               // the match length
+     *         },
+     *         {                                    // second substring match is `"bcd"`
+     *             new Long[] {1L, 3L},             // in `gs("key1")` it is located between indices `1` and `3`
+     *             new Long[] {0L, 2L},             // and in `gs("key2")` - in between `0` and `2`
+     *             3L                               // the match length
+     *         }
+     *     },
+     *     "len", 6                                 // total length of the all matches found
+     * );
      * }
- * The result indicates that the first substring match is gs("1234") in - * key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * gs("bcd") in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. */ CompletableFuture> lcsIdxWithMatchLen( GlideString key1, GlideString key2, long minMatchLen); diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index cf2b77401e..4620f375b8 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -225,6 +225,7 @@ import command_request.CommandRequestOuterClass.Command.ArgsArray; import command_request.CommandRequestOuterClass.RequestType; import command_request.CommandRequestOuterClass.Transaction; +import glide.api.commands.StringBaseCommands; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.FlushMode; import glide.api.models.commands.GetExOptions; @@ -3038,7 +3039,7 @@ public T zintercard(@NonNull ArgType[] keys, long limit) { * , and stores the result in destination. If destination already * exists, it is overwritten. Otherwise, a new sorted set will be created.
* To perform a zinterstore operation while specifying aggregation settings, use - * {@link #zinterstore(Object, KeysOrWeightedKeys, Aggregate)}. + * {@link #zinterstore(String, KeysOrWeightedKeys, Aggregate)}. * * @see valkey.io for more details. * @param destination The key of the destination sorted set. @@ -3064,7 +3065,7 @@ public T zinterstore( * , and stores the result in destination. If destination already * exists, it is overwritten. Otherwise, a new sorted set will be created.
* To perform a zinterstore operation while specifying aggregation settings, use - * {@link #zinterstore(Object, KeysOrWeightedKeys, Aggregate)}. + * {@link #zinterstore(GlideString, KeysOrWeightedKeysBinary, Aggregate)}. * * @see valkey.io for more details. * @param destination The key of the destination sorted set. @@ -6243,8 +6244,8 @@ public T functionDelete(@NonNull ArgType libName) { } /** - * Returns the longest common subsequence between strings stored at key1 and - * key2. + * Returns all the longest common subsequences combined between strings stored at key1 + * and key2. * * @since Valkey 7.0 and above. * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type @@ -6252,9 +6253,9 @@ public T functionDelete(@NonNull ArgType libName) { * @see valkey.io for details. * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. - * @return Command Response - A String containing the longest common subsequence - * between the 2 strings. An empty String is returned if the keys do not exist or - * have no common subsequences. + * @return Command Response - A String containing all the longest common subsequences + * combined between the 2 strings. An empty String/GlideString is + * returned if the keys do not exist or have no common subsequences. */ public T lcs(@NonNull ArgType key1, @NonNull ArgType key2) { checkTypeOrThrow(key1); @@ -6263,8 +6264,8 @@ public T lcs(@NonNull ArgType key1, @NonNull ArgType key2) { } /** - * Returns the length of the longest common subsequence between strings stored at key1 - * and key2. + * Returns the total length of all the longest common subsequences between strings stored at + * key1 and key2. * * @since Valkey 7.0 and above. * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type @@ -6272,7 +6273,8 @@ public T lcs(@NonNull ArgType key1, @NonNull ArgType key2) { * @see valkey.io for details. * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. - * @return Command Response - The length of the longest common subsequence between the 2 strings. + * @return Command Response - The total length of all the longest common subsequences between the + * 2 strings. */ public T lcsLen(@NonNull ArgType key1, @NonNull ArgType key2) { checkTypeOrThrow(key1); @@ -6394,35 +6396,16 @@ public T sunion(@NonNull ArgType[] keys) { * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. * @return Command Response - A Map containing the indices of the longest common - * subsequence between the 2 strings and the length of the longest common subsequence. The - * resulting map contains two keys, "matches" and "len": + * subsequence between the 2 strings and the total length of all the longest common + * subsequences. The resulting map contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the string "abcd123" and key2 - * holds the string "bcdef123" then the sample result would be - *
{@code
-     * new Long[][][] {
-     *     {
-     *         {4L, 6L},
-     *         {5L, 7L}
-     *     },
-     *     {
-     *         {1L, 3L},
-     *         {0L, 2L}
-     *     }
-     * }
-     * }
- * The result indicates that the first substring match is "123" in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * "bcd" in key1 at index 1 to 3 which matches - * the substring in key2 at index 0 to 2. + * See example of {@link StringBaseCommands#lcsIdx(String, String)} for more details. */ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2) { checkTypeOrThrow(key1); @@ -6432,8 +6415,8 @@ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2) { } /** - * Returns the indices and length of the longest common subsequence between strings stored at - * key1 and key2. + * Returns the indices and the total length of all the longest common subsequences between strings + * stored at key1 and key2. * * @since Valkey 7.0 and above. * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type @@ -6443,35 +6426,17 @@ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2) { * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return Command Response - A Map containing the indices of the longest common - * subsequence between the 2 strings and the length of the longest common subsequence. The - * resulting map contains two keys, "matches" and "len": + * subsequence between the 2 strings and the total length of all the longest common + * subsequences. The resulting map contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. This value doesn't count towards the + * minMatchLen filter. *
  • "matches" is mapped to a three dimensional Long array that stores pairs * of indices that represent the location of the common subsequences in the strings held * by key1 and key2. *
- * - * @example If key1 holds the string "abcd123" and key2 - * holds the string "bcdef123" then the sample result would be - *
{@code
-     * new Long[][][] {
-     *     {
-     *         {4L, 6L},
-     *         {5L, 7L}
-     *     },
-     *     {
-     *         {1L, 3L},
-     *         {0L, 2L}
-     *     }
-     * }
-     * }
- * The result indicates that the first substring match is "123" in key1 - * at index 4 to 6 which matches the substring in key2 - * at index 5 to 7. And the second substring match is - * "bcd" in key1 at index 1 to 3 which matches - * the substring in key2 at index 0 to 2. + * See example of {@link StringBaseCommands#lcsIdx(String, String, long)} for more details. */ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2, long minMatchLen) { checkTypeOrThrow(key1); @@ -6488,7 +6453,7 @@ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2, long min } /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -6498,39 +6463,17 @@ public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2, long min * @param key1 The key that stores the first string. * @param key2 The key that stores the second string. * @return Command Response - A Map containing the indices of the longest common - * subsequence between the 2 strings and the length of the longest common subsequence. The + * subsequence between the 2 strings and the lengths of the longest common subsequences. The * resulting map contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the string "abcd1234" and key2 - * holds the string "bcdef1234" then the sample result would be - *
{@code
-     * new Object[] {
-     *     new Object[] {
-     *         new Long[] {4L, 7L},
-     *         new Long[] {5L, 8L},
-     *         4L
-     *     },
-     *     new Object[] {
-     *         new Long[] {1L, 3L},
-     *         new Long[] {0L, 2L},
-     *         3L
-     *     }
-     * }
-     * }
- * The result indicates that the first substring match is "1234" in key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * "bcd" in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. + * See example of {@link StringBaseCommands#lcsIdxWithMatchLen(String, String)} for more + * details. */ public T lcsIdxWithMatchLen(@NonNull ArgType key1, @NonNull ArgType key2) { checkTypeOrThrow(key1); @@ -6546,7 +6489,7 @@ public T lcsIdxWithMatchLen(@NonNull ArgType key1, @NonNull ArgType ke } /** - * Returns the indices and length of the longest common subsequence between strings stored at + * Returns the indices and lengths of the longest common subsequences between strings stored at * key1 and key2. * * @since Valkey 7.0 and above. @@ -6557,39 +6500,17 @@ public T lcsIdxWithMatchLen(@NonNull ArgType key1, @NonNull ArgType ke * @param key2 The key that stores the second string. * @param minMatchLen The minimum length of matches to include in the result. * @return Command Response - A Map containing the indices of the longest common - * subsequence between the 2 strings and the length of the longest common subsequence. The + * subsequence between the 2 strings and the lengths of the longest common subsequences. The * resulting map contains two keys, "matches" and "len": *
    - *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings - * stored as Long. - *
  • "matches" is mapped to a three dimensional Long array that stores pairs - * of indices that represent the location of the common subsequences in the strings held - * by key1 and key2. + *
  • "len" is mapped to the total length of the all longest common subsequences between + * the 2 strings stored as Long. + *
  • "matches" is mapped to a three dimensional array that stores pairs of indices that + * represent the location of the common subsequences in the strings held by key1 + * and key2 and the match length. *
- * - * @example If key1 holds the string "abcd1234" and key2 - * holds the string "bcdef1234" then the sample result would be - *
{@code
-     * new Object[] {
-     *     new Object[] {
-     *         new Long[] { 4L, 7L },
-     *         new Long[] { 5L, 8L },
-     *         4L
-     *     },
-     *     new Object[] {
-     *         new Long[] { 1L, 3L },
-     *         new Long[] { 0L, 2L },
-     *         3L
-     *     }
-     * }
-     * }
- * The result indicates that the first substring match is "1234" in key1 - * at index 4 to 7 which matches the substring in key2 - * at index 5 to 8 and the last element in the array is the - * length of the substring match which is 4. And the second substring match is - * "bcd" in key1 at index 1 to 3 which - * matches the substring in key2 at index 0 to 2 and - * the last element in the array is the length of the substring match which is 3. + * See example of {@link StringBaseCommands#lcsIdxWithMatchLen(String, String, long)} for more + * details. */ public T lcsIdxWithMatchLen( @NonNull ArgType key1, @NonNull ArgType key2, long minMatchLen) { diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4dc45abe4a..705dcbf6b8 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -5625,7 +5625,7 @@ export class BaseClient { * ```typescript * await client.mset({"testKey1": "abcd", "testKey2": "axcd"}); * const result = await client.lcs("testKey1", "testKey2"); - * console.log(result); // Output: 'cd' + * console.log(result); // Output: 'acd' * ``` */ public async lcs(key1: string, key2: string): Promise { @@ -5647,7 +5647,7 @@ export class BaseClient { * ```typescript * await client.mset({"testKey1": "abcd", "testKey2": "axcd"}); * const result = await client.lcsLen("testKey1", "testKey2"); - * console.log(result); // Output: 2 + * console.log(result); // Output: 3 * ``` */ public async lcsLen(key1: string, key2: string): Promise { @@ -5675,6 +5675,8 @@ export class BaseClient { * of indices that represent the location of the common subsequences in the strings held * by `key1` and `key2`. * + * See example for more details. + * * @example * ```typescript * await client.mset({"key1": "ohmytext", "key2": "mynewtext"}); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c7b7746158..9e450936c5 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3334,6 +3334,8 @@ export class BaseTransaction> { * - `"matches"` is mapped to a three dimensional array of integers that stores pairs * of indices that represent the location of the common subsequences in the strings held * by `key1` and `key2`. + * + * See example of {@link BaseClient.lcsIdx|lcsIdx} for more details. */ public lcsIdx( key1: string, From fc5a64ba3eecaf5e35d02de365ef7930c9dc2e2a Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 15 Aug 2024 14:08:07 -0700 Subject: [PATCH 181/236] Node: add `hkeys` command (#2136) * adds HKEYS command --------- Signed-off-by: Adar Ovadia Signed-off-by: Andrew Carbonetto Co-authored-by: Ubuntu Co-authored-by: adarovadya Co-authored-by: Yury-Fridlyand Co-authored-by: Ubuntu Co-authored-by: Adar Ovadia --- CHANGELOG.md | 1 + node/.prettierignore | 1 - node/src/BaseClient.ts | 25 +++++++++++++++++++++++-- node/src/Commands.ts | 7 +++++++ node/src/Transaction.ts | 14 ++++++++++++++ node/tests/SharedTests.ts | 34 ++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 ++ 7 files changed, 81 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12854040b7..11da86683d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Java: Added PUBSUB CHANNELS, NUMPAT and NUMSUB commands ([#2105](https://github.com/valkey-io/valkey-glide/pull/2105)) * Java: Added binary support for custom command ([#2109](https://github.com/valkey-io/valkey-glide/pull/2109)) * Node: Added SSCAN command ([#2132](https://github.com/valkey-io/valkey-glide/pull/2132)) +* Node: Added HKEYS command ([#2136](https://github.com/aws/glide-for-redis/pull/2136)) * Node: Added FUNCTION KILL command ([#2114](https://github.com/valkey-io/valkey-glide/pull/2114)) * Node: Update all commands to use `async` ([#2110](https://github.com/valkey-io/valkey-glide/pull/2110)) * Node: Added XAUTOCLAIM command ([#2108](https://github.com/valkey-io/valkey-glide/pull/2108)) diff --git a/node/.prettierignore b/node/.prettierignore index 6ced842400..086fea31e1 100644 --- a/node/.prettierignore +++ b/node/.prettierignore @@ -1,6 +1,5 @@ # ignore that dir, because there are a lot of files which we don't manage, e.g. json files in cargo crates rust-client/* -*.md # unignore specific files !rust-client/package.json !rust-client/tsconfig.json diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 705dcbf6b8..83042ebdce 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -91,6 +91,7 @@ import { createHGetAll, createHIncrBy, createHIncrByFloat, + createHKeys, createHLen, createHMGet, createHRandField, @@ -1487,7 +1488,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of the hget method on an-existing field - * await client.hset("my_hash", "field"); + * await client.hset("my_hash", {"field": "value"}); * const result = await client.hget("my_hash", "field"); * console.log(result); // Output: "value" * ``` @@ -1515,7 +1516,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of the hset method - * const result = await client.hset("my_hash", \{"field": "value", "field2": "value2"\}); + * const result = await client.hset("my_hash", {"field": "value", "field2": "value2"}); * console.log(result); // Output: 2 - Indicates that 2 fields were successfully set in the hash "my_hash". * ``` */ @@ -1526,6 +1527,26 @@ export class BaseClient { return this.createWritePromise(createHSet(key, fieldValueMap)); } + /** + * Returns all field names in the hash stored at `key`. + * + * @see {@link https://valkey.io/commands/hkeys/|valkey.io} for details. + * + * @param key - The key of the hash. + * @returns A list of field names for the hash, or an empty list when the key does not exist. + * + * @example + * ```typescript + * // Example usage of the hkeys method: + * await client.hset("my_hash", {"field1": "value1", "field2": "value2", "field3": "value3"}); + * const result = await client.hkeys("my_hash"); + * console.log(result); // Output: ["field1", "field2", "field3"] - Returns all the field names stored in the hash "my_hash". + * ``` + */ + public hkeys(key: string): Promise { + return this.createWritePromise(createHKeys(key)); + } + /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8a6fd01ae0..0ed3310fe8 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -421,6 +421,13 @@ export function createHSet( ); } +/** + * @internal + */ +export function createHKeys(key: string): command_request.Command { + return createCommand(RequestType.HKeys, [key]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 9e450936c5..e83233746a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -115,6 +115,7 @@ import { createHGetAll, createHIncrBy, createHIncrByFloat, + createHKeys, createHLen, createHMGet, createHRandField, @@ -742,6 +743,19 @@ export class BaseTransaction> { return this.addAndReturn(createHSet(key, fieldValueMap)); } + /** + * Returns all field names in the hash stored at `key`. + * + * @see {@link https://valkey.io/commands/hkeys/|valkey.io} for details. + * + * @param key - The key of the hash. + * + * Command Response - A list of field names for the hash, or an empty list when the key does not exist. + */ + public hkeys(key: string): T { + return this.addAndReturn(createHKeys(key)); + } + /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1a0e4d2866..4720c77d3c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1278,6 +1278,40 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `testing hkeys with exiting, an non exising key and error request key_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const key2 = uuidv4(); + const field1 = uuidv4(); + const field2 = uuidv4(); + const value = uuidv4(); + const value2 = uuidv4(); + const fieldValueMap = { + [field1]: value, + [field2]: value2, + }; + + // set up hash with two keys/values + expect(await client.hset(key, fieldValueMap)).toEqual(2); + expect(await client.hkeys(key)).toEqual([field1, field2]); + + // remove one key + expect(await client.hdel(key, [field1])).toEqual(1); + expect(await client.hkeys(key)).toEqual([field2]); + + // non-existing key returns an empty list + expect(await client.hkeys("nonExistingKey")).toEqual([]); + + // Key exists, but it is not a hash + expect(await client.set(key2, value)).toEqual("OK"); + await expect(client.hkeys(key2)).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `hscan test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2faf78335d..83ac24c825 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -744,6 +744,8 @@ export async function transactionTest( responseData.push(["hsetnx(key4, field, value)", false]); baseTransaction.hvals(key4); responseData.push(["hvals(key4)", [value]]); + baseTransaction.hkeys(key4); + responseData.push(["hkeys(key4)", [field]]); baseTransaction.hget(key4, field); responseData.push(["hget(key4, field)", value]); baseTransaction.hgetall(key4); From 1195c8254eeed614bb152c623489f70ee6901f16 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 16 Aug 2024 12:51:04 -0700 Subject: [PATCH 182/236] Node: Add `XINFO GROUPS` command. (#2122) Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 42 +++++++- node/src/Commands.ts | 5 + node/src/Transaction.ts | 15 +++ node/tests/SharedTests.ts | 192 +++++++++++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 2 + 6 files changed, 255 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11da86683d..bf77700860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) * Java: Added PUBSUB CHANNELS, NUMPAT and NUMSUB commands ([#2105](https://github.com/valkey-io/valkey-glide/pull/2105)) * Java: Added binary support for custom command ([#2109](https://github.com/valkey-io/valkey-glide/pull/2109)) * Node: Added SSCAN command ([#2132](https://github.com/valkey-io/valkey-glide/pull/2132)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 83042ebdce..9c78351a34 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -34,7 +34,7 @@ import { GeoUnit, GeospatialData, InsertPosition, - KeyWeight, // eslint-disable-line @typescript-eslint/no-unused-vars + KeyWeight, LPosOptions, ListDirection, MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -176,6 +176,7 @@ import { createXGroupDelConsumer, createXGroupDestroy, createXInfoConsumers, + createXInfoGroups, createXInfoStream, createXLen, createXPending, @@ -4405,6 +4406,45 @@ export class BaseClient { return this.createWritePromise(createXInfoConsumers(key, group)); } + /** + * Returns the list of all consumer groups and their attributes for the stream stored at `key`. + * + * @see {@link https://valkey.io/commands/xinfo-groups/|valkey.io} for details. + * + * @param key - The key of the stream. + * @returns An array of maps, where each mapping represents the + * attributes of a consumer group for the stream at `key`. + * @example + * ```typescript + *
{@code
+     * const result = await client.xinfoGroups("my_stream");
+     * console.log(result); // Output:
+     * // [
+     * //     {
+     * //         "name": "mygroup",
+     * //         "consumers": 2,
+     * //         "pending": 2,
+     * //         "last-delivered-id": "1638126030001-0",
+     * //         "entries-read": 2,                       // Added in version 7.0.0
+     * //         "lag": 0                                 // Added in version 7.0.0
+     * //     },
+     * //     {
+     * //         "name": "some-other-group",
+     * //         "consumers": 1,
+     * //         "pending": 0,
+     * //         "last-delivered-id": "0-0",
+     * //         "entries-read": null,                    // Added in version 7.0.0
+     * //         "lag": 1                                 // Added in version 7.0.0
+     * //     }
+     * // ]
+     * ```
+     */
+    public async xinfoGroups(
+        key: string,
+    ): Promise[]> {
+        return this.createWritePromise(createXInfoGroups(key));
+    }
+
     /**
      * Changes the ownership of a pending message.
      *
diff --git a/node/src/Commands.ts b/node/src/Commands.ts
index 0ed3310fe8..936d0eab73 100644
--- a/node/src/Commands.ts
+++ b/node/src/Commands.ts
@@ -2453,6 +2453,11 @@ export function createXInfoStream(
     return createCommand(RequestType.XInfoStream, args);
 }
 
+/** @internal */
+export function createXInfoGroups(key: string): command_request.Command {
+    return createCommand(RequestType.XInfoGroups, [key]);
+}
+
 /**
  * @internal
  */
diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts
index e83233746a..986aabff9b 100644
--- a/node/src/Transaction.ts
+++ b/node/src/Transaction.ts
@@ -212,6 +212,7 @@ import {
     createXGroupDelConsumer,
     createXGroupDestroy,
     createXInfoConsumers,
+    createXInfoGroups,
     createXInfoStream,
     createXLen,
     createXPending,
@@ -2331,6 +2332,20 @@ export class BaseTransaction> {
         return this.addAndReturn(createXInfoStream(key, fullOptions ?? false));
     }
 
+    /**
+     * Returns the list of all consumer groups and their attributes for the stream stored at `key`.
+     *
+     * @see {@link https://valkey.io/commands/xinfo-groups/|valkey.io} for details.
+     *
+     * @param key - The key of the stream.
+     *
+     * Command Response -  An `Array` of `Records`, where each mapping represents the
+     *     attributes of a consumer group for the stream at `key`.
+     */
+    public xinfoGroups(key: string): T {
+        return this.addAndReturn(createXInfoGroups(key));
+    }
+
     /** Returns the server time.
      * @see {@link https://valkey.io/commands/time/|valkey.io} for details.
      *
diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts
index 4720c77d3c..49833cbd24 100644
--- a/node/tests/SharedTests.ts
+++ b/node/tests/SharedTests.ts
@@ -5570,7 +5570,6 @@ export function runBaseTests(config: {
                     "last-entry": (string | number | string[])[];
                     groups: number;
                 };
-                console.log(result);
 
                 // verify result:
                 expect(result.length).toEqual(1);
@@ -8232,6 +8231,197 @@ export function runBaseTests(config: {
         config.timeout,
     );
 
+    it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
+        `xinfogroups xinfo groups %p`,
+        async (protocol) => {
+            await runTest(async (client: BaseClient, cluster) => {
+                const key = uuidv4();
+                const stringKey = uuidv4();
+                const groupName1 = uuidv4();
+                const consumer1 = uuidv4();
+                const streamId1 = "0-1";
+                const streamId2 = "0-2";
+                const streamId3 = "0-3";
+
+                expect(
+                    await client.xgroupCreate(key, groupName1, "0-0", {
+                        mkStream: true,
+                    }),
+                ).toEqual("OK");
+
+                // one empty group exists
+                expect(await client.xinfoGroups(key)).toEqual(
+                    cluster.checkIfServerVersionLessThan("7.0.0")
+                        ? [
+                              {
+                                  name: groupName1,
+                                  consumers: 0,
+                                  pending: 0,
+                                  "last-delivered-id": "0-0",
+                              },
+                          ]
+                        : [
+                              {
+                                  name: groupName1,
+                                  consumers: 0,
+                                  pending: 0,
+                                  "last-delivered-id": "0-0",
+                                  "entries-read": null,
+                                  lag: 0,
+                              },
+                          ],
+                );
+
+                expect(
+                    await client.xadd(
+                        key,
+                        [
+                            ["entry1_field1", "entry1_value1"],
+                            ["entry1_field2", "entry1_value2"],
+                        ],
+                        { id: streamId1 },
+                    ),
+                ).toEqual(streamId1);
+
+                expect(
+                    await client.xadd(
+                        key,
+                        [
+                            ["entry2_field1", "entry2_value1"],
+                            ["entry2_field2", "entry2_value2"],
+                        ],
+                        { id: streamId2 },
+                    ),
+                ).toEqual(streamId2);
+
+                expect(
+                    await client.xadd(
+                        key,
+                        [["entry3_field1", "entry3_value1"]],
+                        { id: streamId3 },
+                    ),
+                ).toEqual(streamId3);
+
+                // same as previous check, bug lag = 3, there are 3 messages unread
+                expect(await client.xinfoGroups(key)).toEqual(
+                    cluster.checkIfServerVersionLessThan("7.0.0")
+                        ? [
+                              {
+                                  name: groupName1,
+                                  consumers: 0,
+                                  pending: 0,
+                                  "last-delivered-id": "0-0",
+                              },
+                          ]
+                        : [
+                              {
+                                  name: groupName1,
+                                  consumers: 0,
+                                  pending: 0,
+                                  "last-delivered-id": "0-0",
+                                  "entries-read": null,
+                                  lag: 3,
+                              },
+                          ],
+                );
+
+                expect(
+                    await client.customCommand([
+                        "XREADGROUP",
+                        "GROUP",
+                        groupName1,
+                        consumer1,
+                        "STREAMS",
+                        key,
+                        ">",
+                    ]),
+                ).toEqual({
+                    [key]: {
+                        [streamId1]: [
+                            ["entry1_field1", "entry1_value1"],
+                            ["entry1_field2", "entry1_value2"],
+                        ],
+                        [streamId2]: [
+                            ["entry2_field1", "entry2_value1"],
+                            ["entry2_field2", "entry2_value2"],
+                        ],
+                        [streamId3]: [["entry3_field1", "entry3_value1"]],
+                    },
+                });
+                // after reading, `lag` is reset, and `pending`, consumer count and last ID are set
+                expect(await client.xinfoGroups(key)).toEqual(
+                    cluster.checkIfServerVersionLessThan("7.0.0")
+                        ? [
+                              {
+                                  name: groupName1,
+                                  consumers: 1,
+                                  pending: 3,
+                                  "last-delivered-id": streamId3,
+                              },
+                          ]
+                        : [
+                              {
+                                  name: groupName1,
+                                  consumers: 1,
+                                  pending: 3,
+                                  "last-delivered-id": streamId3,
+                                  "entries-read": 3,
+                                  lag: 0,
+                              },
+                          ],
+                );
+
+                expect(
+                    await client.customCommand([
+                        "XACK",
+                        key,
+                        groupName1,
+                        streamId1,
+                    ]),
+                ).toEqual(1);
+                // once message ack'ed, pending counter decreased
+                expect(await client.xinfoGroups(key)).toEqual(
+                    cluster.checkIfServerVersionLessThan("7.0.0")
+                        ? [
+                              {
+                                  name: groupName1,
+                                  consumers: 1,
+                                  pending: 2,
+                                  "last-delivered-id": streamId3,
+                              },
+                          ]
+                        : [
+                              {
+                                  name: groupName1,
+                                  consumers: 1,
+                                  pending: 2,
+                                  "last-delivered-id": streamId3,
+                                  "entries-read": 3,
+                                  lag: 0,
+                              },
+                          ],
+                );
+
+                // key exists, but it is not a stream
+                expect(await client.set(stringKey, "foo")).toEqual("OK");
+                await expect(client.xinfoGroups(stringKey)).rejects.toThrow(
+                    RequestError,
+                );
+
+                // Passing a non-existing key raises an error
+                const key2 = uuidv4();
+                await expect(client.xinfoGroups(key2)).rejects.toThrow(
+                    RequestError,
+                );
+                // create a second stream
+                await client.xadd(key2, [["a", "b"]]);
+                // no group yet exists
+                expect(await client.xinfoGroups(key2)).toEqual([]);
+            }, protocol);
+        },
+        config.timeout,
+    );
+
     it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
         `xpending test_%p`,
         async (protocol) => {
diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts
index 83ac24c825..29147cdac4 100644
--- a/node/tests/TestUtilities.ts
+++ b/node/tests/TestUtilities.ts
@@ -1101,6 +1101,8 @@ export async function transactionTest(
         'xtrim(key9, { method: "minid", threshold: "0-2", exact: true }',
         1,
     ]);
+    baseTransaction.xinfoGroups(key9);
+    responseData.push(["xinfoGroups(key9)", []]);
     baseTransaction.xgroupCreate(key9, groupName1, "0-0");
     responseData.push(['xgroupCreate(key9, groupName1, "0-0")', "OK"]);
     baseTransaction.xgroupCreate(key9, groupName2, "0-0", { mkStream: true });

From 048812b0b2d47874f79bdc211e423f07a0686d79 Mon Sep 17 00:00:00 2001
From: Yi-Pin Chen 
Date: Fri, 16 Aug 2024 18:56:07 -0700
Subject: [PATCH 183/236] Node: added DUMP and RESTORE commands (#2126)

* Node: added DUMP and RESTORE command

Signed-off-by: Yi-Pin Chen 
---
 CHANGELOG.md                                  |   1 +
 .../api/commands/GenericBaseCommands.java     |  13 ++-
 .../glide/api/models/BaseTransaction.java     |  23 ++--
 .../api/models/commands/RestoreOptions.java   |   3 +-
 node/npm/glide/index.ts                       |   2 +
 node/src/BaseClient.ts                        |  78 +++++++++++++
 node/src/Commands.ts                          |  71 +++++++++++
 node/tests/SharedTests.ts                     | 110 ++++++++++++++++++
 python/python/glide/async_commands/core.py    |   2 +
 .../glide/async_commands/transaction.py       |   2 +
 10 files changed, 287 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf77700860..54312ceb4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,7 @@
 * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053))
 * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076))
 * Node: Added WAIT command ([#2113](https://github.com/valkey-io/valkey-glide/pull/2113))
+* Node: Added DUMP and RESTORE commands ([#2126](https://github.com/valkey-io/valkey-glide/pull/2126))
 * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022))
 * Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025))
 * Node: Added ZRANGESTORE command ([#2068](https://github.com/valkey-io/valkey-glide/pull/2068))
diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java
index a2d4c92a7d..9eb5bc9a45 100644
--- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java
+++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java
@@ -1153,8 +1153,8 @@ CompletableFuture pexpireAt(
      * user.
      *
      * @see valkey.io for details.
-     * @param key The key of the set.
-     * @return The serialized value of a set.
+ * @param key The key to serialize. + * @return The serialized value of the data stored at key.
* If key does not exist, null will be returned. * @example *
{@code
@@ -1171,10 +1171,10 @@ CompletableFuture pexpireAt(
      * deserializing the provided serialized value (obtained via {@link #dump}).
      *
      * @see valkey.io for details.
-     * @param key The key of the set.
+     * @param key The key to create.
      * @param ttl The expiry time (in milliseconds). If 0, the key will
      *     persist.
-     * @param value The serialized value.
+     * @param value The serialized value to deserialize and assign to key.
      * @return Return OK if successfully create a key with a value
      *      .
      * @example
@@ -1189,11 +1189,12 @@ CompletableFuture pexpireAt(
      * Create a key associated with a value that is obtained by
      * deserializing the provided serialized value (obtained via {@link #dump}).
      *
+     * @apiNote IDLETIME and FREQ modifiers cannot be set at the same time.
      * @see valkey.io for details.
-     * @param key The key of the set.
+     * @param key The key to create.
      * @param ttl The expiry time (in milliseconds). If 0, the key will
      *     persist.
-     * @param value The serialized value.
+     * @param value The serialized value to deserialize and assign to key.
      * @param restoreOptions The restore options. See {@link RestoreOptions}.
      * @return Return OK if successfully create a key with a value
      *      .
diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java
index 4620f375b8..1130e30f1c 100644
--- a/java/client/src/main/java/glide/api/models/BaseTransaction.java
+++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java
@@ -5230,9 +5230,9 @@ public  T copy(@NonNull ArgType source, @NonNull ArgType destination) {
      * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type
      *     will throw {@link IllegalArgumentException}.
      * @see valkey.io for details.
-     * @param key The key of the set.
-     * @return Command Response - The serialized value of a set. If key does not exist,
-     *     null will be returned.
+     * @param key The key to serialize.
+     * @return Command Response - The serialized value of the data stored at key.
+ * If key does not exist, null will be returned. */ public T dump(@NonNull ArgType key) { checkTypeOrThrow(key); @@ -5247,12 +5247,12 @@ public T dump(@NonNull ArgType key) { * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. * @see valkey.io for details. - * @param key The key of the set. + * @param key The key to create. * @param ttl The expiry time (in milliseconds). If 0, the key will * persist. - * @param value The serialized value. - * @return Command Response - Return OK if successfully create a key - * with a value. + * @param value The serialized value to deserialize and assign to key. + * @return Command Response - Return OK if the key was successfully + * restored with a value. */ public T restore(@NonNull ArgType key, long ttl, @NonNull byte[] value) { checkTypeOrThrow(key); @@ -5267,14 +5267,15 @@ public T restore(@NonNull ArgType key, long ttl, @NonNull byte[] value * * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. + * @apiNote IDLETIME and FREQ modifiers cannot be set at the same time. * @see valkey.io for details. - * @param key The key of the set. + * @param key The key to create. * @param ttl The expiry time (in milliseconds). If 0, the key will * persist. - * @param value The serialized value. + * @param value The serialized value to deserialize and assign to key. * @param restoreOptions The restore options. See {@link RestoreOptions}. - * @return Command Response - Return OK if successfully create a key - * with a value. + * @return Command Response - Return OK if the key was successfully + * restored with a value. */ public T restore( @NonNull ArgType key, diff --git a/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java b/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java index 4a069e5a92..a35bd40807 100644 --- a/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java @@ -11,9 +11,10 @@ /** * Optional arguments to {@link GenericBaseCommands#restore(GlideString, long, byte[], - * RestoreOptions)} + * RestoreOptions)}. * * @see valkey.io + * @apiNote IDLETIME and FREQ modifiers cannot be set at the same time. */ @Getter @Builder diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 4cf1855926..ecf8bec0ba 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -113,6 +113,7 @@ function initialize() { TimeUnit, RouteByAddress, Routes, + RestoreOptions, SingleNodeRoute, PeriodicChecksManualInterval, PeriodicChecks, @@ -210,6 +211,7 @@ function initialize() { ReturnTypeXinfoStream, RouteByAddress, Routes, + RestoreOptions, SingleNodeRoute, PeriodicChecksManualInterval, PeriodicChecks, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9c78351a34..e304eab33a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -41,6 +41,7 @@ import { RangeByIndex, RangeByLex, RangeByScore, + RestoreOptions, ReturnTypeXinfoStream, ScoreFilter, SearchOrigin, @@ -68,6 +69,7 @@ import { createDecr, createDecrBy, createDel, + createDump, createExists, createExpire, createExpireAt, @@ -140,6 +142,7 @@ import { createRPushX, createRename, createRenameNX, + createRestore, createSAdd, createSCard, createSDiff, @@ -1094,6 +1097,81 @@ export class BaseClient { return this.createWritePromise(createDel(keys)); } + /** + * Serialize the value stored at `key` in a Valkey-specific format and return it to the user. + * + * @See {@link https://valkey.io/commands/dump/|valkey.io} for details. + * + * @param key - The `key` to serialize. + * @returns The serialized value of the data stored at `key`. If `key` does not exist, `null` will be returned. + * + * @example + * ```typescript + * let result = await client.dump("myKey"); + * console.log(result); // Output: the serialized value of "myKey" + * ``` + * + * @example + * ```typescript + * result = await client.dump("nonExistingKey"); + * console.log(result); // Output: `null` + * ``` + */ + public async dump(key: GlideString): Promise { + return this.createWritePromise(createDump(key), { + decoder: Decoder.Bytes, + }); + } + + /** + * Create a `key` associated with a `value` that is obtained by deserializing the provided + * serialized `value` (obtained via {@link dump}). + * + * @See {@link https://valkey.io/commands/restore/|valkey.io} for details. + * @remarks `options.idletime` and `options.frequency` modifiers cannot be set at the same time. + * + * @param key - The `key` to create. + * @param ttl - The expiry time (in milliseconds). If `0`, the `key` will persist. + * @param value - The serialized value to deserialize and assign to `key`. + * @param options - (Optional) Restore options {@link RestoreOptions}. + * @returns Return "OK" if the `key` was successfully restored with a `value`. + * + * @example + * ```typescript + * const result = await client.restore("myKey", 0, value); + * console.log(result); // Output: "OK" + * ``` + * + * @example + * ```typescript + * const result = await client.restore("myKey", 1000, value, {replace: true, absttl: true}); + * console.log(result); // Output: "OK" + * ``` + * + * @example + * ```typescript + * const result = await client.restore("myKey", 0, value, {replace: true, idletime: 10}); + * console.log(result); // Output: "OK" + * ``` + * + * @example + * ```typescript + * const result = await client.restore("myKey", 0, value, {replace: true, frequency: 10}); + * console.log(result); // Output: "OK" + * ``` + */ + public async restore( + key: GlideString, + ttl: number, + value: Buffer, + options?: RestoreOptions, + ): Promise<"OK"> { + return this.createWritePromise( + createRestore(key, ttl, value, options), + { decoder: Decoder.String }, + ); + } + /** Retrieve the values of multiple keys. * * @see {@link https://valkey.io/commands/mget/|valkey.io} for details. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 936d0eab73..b2c7e09989 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2815,6 +2815,77 @@ export function createMove( return createCommand(RequestType.Move, [key, dbIndex.toString()]); } +/** + * @internal + */ +export function createDump(key: GlideString): command_request.Command { + return createCommand(RequestType.Dump, [key]); +} + +/** + * Optional arguments for `RESTORE` command. + * + * @See {@link https://valkey.io/commands/restore/|valkey.io} for details. + * @remarks `IDLETIME` and `FREQ` modifiers cannot be set at the same time. + */ +export type RestoreOptions = { + /** + * Set to `true` to replace the key if it exists. + */ + replace?: boolean; + /** + * Set to `true` to specify that `ttl` argument of {@link BaseClient.restore} represents + * an absolute Unix timestamp (in milliseconds). + */ + absttl?: boolean; + /** + * Set the `IDLETIME` option with object idletime to the given key. + */ + idletime?: number; + /** + * Set the `FREQ` option with object frequency to the given key. + */ + frequency?: number; +}; + +/** + * @internal + */ +export function createRestore( + key: GlideString, + ttl: number, + value: GlideString, + options?: RestoreOptions, +): command_request.Command { + const args: GlideString[] = [key, ttl.toString(), value]; + + if (options) { + if (options.idletime !== undefined && options.frequency !== undefined) { + throw new Error( + `syntax error: both IDLETIME and FREQ cannot be set at the same time.`, + ); + } + + if (options.replace) { + args.push("REPLACE"); + } + + if (options.absttl) { + args.push("ABSTTL"); + } + + if (options.idletime !== undefined) { + args.push("IDLETIME", options.idletime.toString()); + } + + if (options.frequency !== undefined) { + args.push("FREQ", options.frequency.toString()); + } + } + + return createCommand(RequestType.Restore, args); +} + /** * Optional arguments to LPOS command. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 49833cbd24..7eaaa3cd9c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5776,6 +5776,116 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "dump and restore test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const key3 = "{key}-3" + uuidv4(); + const nonExistingkey = "{nonExistingkey}-" + uuidv4(); + const value = "orange"; + const valueEncode = Buffer.from(value); + + expect(await client.set(key1, value)).toEqual("OK"); + + // Dump non-existing key + expect(await client.dump(nonExistingkey)).toBeNull(); + + // Dump existing key + const data = (await client.dump(key1)) as Buffer; + expect(data).not.toBeNull(); + + // Restore to a new key without option + expect(await client.restore(key2, 0, data)).toEqual("OK"); + expect(await client.get(key2, Decoder.String)).toEqual(value); + expect(await client.get(key2, Decoder.Bytes)).toEqual( + valueEncode, + ); + + // Restore to an existing key + await expect(client.restore(key2, 0, data)).rejects.toThrow( + "BUSYKEY: Target key name already exists.", + ); + + // Restore with `REPLACE` and existing key holding different value + expect(await client.sadd(key3, ["a"])).toEqual(1); + expect( + await client.restore(key3, 0, data, { replace: true }), + ).toEqual("OK"); + + // Restore with `REPLACE` option + expect( + await client.restore(key2, 0, data, { replace: true }), + ).toEqual("OK"); + + // Restore with `REPLACE`, `ABSTTL`, and positive TTL + expect( + await client.restore(key2, 1000, data, { + replace: true, + absttl: true, + }), + ).toEqual("OK"); + + // Restore with `REPLACE`, `ABSTTL`, and negative TTL + await expect( + client.restore(key2, -10, data, { + replace: true, + absttl: true, + }), + ).rejects.toThrow("Invalid TTL value"); + + // Restore with REPLACE and positive idletime + expect( + await client.restore(key2, 0, data, { + replace: true, + idletime: 10, + }), + ).toEqual("OK"); + + // Restore with REPLACE and negative idletime + await expect( + client.restore(key2, 0, data, { + replace: true, + idletime: -10, + }), + ).rejects.toThrow("Invalid IDLETIME value"); + + // Restore with REPLACE and positive frequency + expect( + await client.restore(key2, 0, data, { + replace: true, + frequency: 10, + }), + ).toEqual("OK"); + + // Restore with REPLACE and negative frequency + await expect( + client.restore(key2, 0, data, { + replace: true, + frequency: -10, + }), + ).rejects.toThrow("Invalid FREQ value"); + + // Restore only uses IDLETIME or FREQ modifiers + // Error will be raised if both options are set + await expect( + client.restore(key2, 0, data, { + replace: true, + idletime: 10, + frequency: 10, + }), + ).rejects.toThrow("syntax error"); + + // Restore with checksumto error + await expect( + client.restore(key2, 0, valueEncode, { replace: true }), + ).rejects.toThrow("DUMP payload version or checksum are wrong"); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "pfadd test_%p", async (protocol) => { diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index c61f069a5f..d31a1a428c 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -6012,6 +6012,8 @@ async def restore( See https://valkey.io/commands/restore for more details. + Note: `IDLETIME` and `FREQ` modifiers cannot be set at the same time. + Args: key (TEncodable): The `key` to create. ttl (int): The expiry time (in milliseconds). If `0`, the `key` will persist. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 1573d51c03..dda2d58814 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -2108,6 +2108,8 @@ def restore( See https://valkey.io/commands/restore for more details. + Note: `IDLETIME` and `FREQ` modifiers cannot be set at the same time. + Args: key (TEncodable): The `key` to create. ttl (int): The expiry time (in milliseconds). If `0`, the `key` will persist. From 850a265c01cc4588794d675f61492ff540a3e92a Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Mon, 19 Aug 2024 09:34:02 +0300 Subject: [PATCH 184/236] Node: add bytes support to append getrange hget mget hvals (#2150) * change signatures of append, getrange, hget, hvals, mget Signed-off-by: lior sventitzky * node: change test getrange Signed-off-by: lior sventitzky * node: change append hvals mget to bytes Signed-off-by: lior sventitzky * fix lint Signed-off-by: lior sventitzky * change hget field type and decoder docs Signed-off-by: lior sventitzky --------- Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 39 +++++++++++++++++------ node/src/Commands.ts | 14 ++++----- node/tests/SharedTests.ts | 65 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e304eab33a..2ab9e6d80d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1010,6 +1010,7 @@ export class BaseClient { * @param key - The key of the string. * @param start - The starting offset. * @param end - The ending offset. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns A substring extracted from the value stored at `key`. * * @example @@ -1026,11 +1027,14 @@ export class BaseClient { * ``` */ public async getrange( - key: string, + key: GlideString, start: number, end: number, - ): Promise { - return this.createWritePromise(createGetRange(key, start, end)); + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createGetRange(key, start, end), { + decoder: decoder, + }); } /** Set the given key with the given value. Return value is dependent on the passed options. @@ -1178,6 +1182,7 @@ export class BaseClient { * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. * * @param keys - A list of keys to retrieve values for. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns A list of values corresponding to the provided keys. If a key is not found, * its corresponding value in the list will be null. * @@ -1190,8 +1195,11 @@ export class BaseClient { * console.log(result); // Output: ['value1', 'value2'] * ``` */ - public async mget(keys: string[]): Promise<(string | null)[]> { - return this.createWritePromise(createMGet(keys)); + public async mget( + keys: GlideString[], + decoder?: Decoder, + ): Promise<(GlideString | null)[]> { + return this.createWritePromise(createMGet(keys), { decoder: decoder }); } /** Set multiple keys to multiple values in a single operation. @@ -1562,6 +1570,7 @@ export class BaseClient { * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns the value associated with `field`, or null when `field` is not present in the hash or `key` does not exist. * * @example @@ -1579,8 +1588,14 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public async hget(key: string, field: string): Promise { - return this.createWritePromise(createHGet(key, field)); + public async hget( + key: GlideString, + field: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createHGet(key, field), { + decoder: decoder, + }); } /** Sets the specified fields to their respective values in the hash stored at `key`. @@ -1831,6 +1846,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/hvals/|valkey.io} for more details. * * @param key - The key of the hash. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns a list of values in the hash, or an empty list when the key does not exist. * * @example @@ -1840,8 +1856,11 @@ export class BaseClient { * console.log(result); // Output: ["value1", "value2", "value3"] - Returns all the values stored in the hash "my_hash". * ``` */ - public async hvals(key: string): Promise { - return this.createWritePromise(createHVals(key)); + public async hvals( + key: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createHVals(key), { decoder: decoder }); } /** @@ -5974,7 +5993,7 @@ export class BaseClient { * // new value of "Hello world" with a length of 11. * ``` */ - public async append(key: string, value: string): Promise { + public async append(key: GlideString, value: GlideString): Promise { return this.createWritePromise(createAppend(key, value)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b2c7e09989..99eddc6767 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -107,7 +107,7 @@ export function createGetDel(key: GlideString): command_request.Command { * @internal */ export function createGetRange( - key: string, + key: GlideString, start: number, end: number, ): command_request.Command { @@ -320,7 +320,7 @@ export function createConfigResetStat(): command_request.Command { /** * @internal */ -export function createMGet(keys: string[]): command_request.Command { +export function createMGet(keys: GlideString[]): command_request.Command { return createCommand(RequestType.MGet, keys); } @@ -402,8 +402,8 @@ export function createConfigSet( * @internal */ export function createHGet( - key: string, - field: string, + key: GlideString, + field: GlideString, ): command_request.Command { return createCommand(RequestType.HGet, [key, field]); } @@ -1221,7 +1221,7 @@ export function createHLen(key: string): command_request.Command { /** * @internal */ -export function createHVals(key: string): command_request.Command { +export function createHVals(key: GlideString): command_request.Command { return createCommand(RequestType.HVals, [key]); } @@ -3623,8 +3623,8 @@ export function createSetRange( /** @internal */ export function createAppend( - key: string, - value: string, + key: GlideString, + value: GlideString, ): command_request.Command { return createCommand(RequestType.Append, [key, value]); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 7eaaa3cd9c..8dea7c45d1 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -349,10 +349,21 @@ export function runBaseTests(config: { [key2]: value, [key3]: value, }; + const valueEncoded = Buffer.from(value); + expect(await client.mset(keyValueList)).toEqual("OK"); expect( await client.mget([key1, key2, "nonExistingKey", key3]), ).toEqual([value, value, null, value]); + + //mget with binary buffers + expect(await client.mset(keyValueList)).toEqual("OK"); + expect( + await client.mget( + [key1, key2, "nonExistingKey", key3], + Decoder.Bytes, + ), + ).toEqual([valueEncoded, valueEncoded, null, valueEncoded]); }, protocol); }, config.timeout, @@ -1217,6 +1228,7 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); const nonStringKey = uuidv4(); + const valueEncoded = Buffer.from("This is a string"); expect(await client.set(key, "This is a string")).toEqual("OK"); expect(await client.getrange(key, 0, 3)).toEqual("This"); @@ -1225,6 +1237,18 @@ export function runBaseTests(config: { "This is a string", ); + // range of binary buffer + expect(await client.set(key, "This is a string")).toEqual("OK"); + expect(await client.getrange(key, 0, 3, Decoder.Bytes)).toEqual( + valueEncoded.subarray(0, 4), + ); + expect( + await client.getrange(key, -3, -1, Decoder.Bytes), + ).toEqual(valueEncoded.subarray(-3, valueEncoded.length)); + expect( + await client.getrange(key, 0, -1, Decoder.Bytes), + ).toEqual(valueEncoded.subarray(0, valueEncoded.length)); + // out of range expect(await client.getrange(key, 10, 100)).toEqual("string"); expect(await client.getrange(key, -200, -3)).toEqual( @@ -1267,12 +1291,25 @@ export function runBaseTests(config: { [field1]: value, [field2]: value, }; + const valueEncoded = Buffer.from(value); + expect(await client.hset(key, fieldValueMap)).toEqual(2); expect(await client.hget(key, field1)).toEqual(value); expect(await client.hget(key, field2)).toEqual(value); expect(await client.hget(key, "nonExistingField")).toEqual( null, ); + + //hget with binary buffer + expect(await client.hget(key, field1, Decoder.Bytes)).toEqual( + valueEncoded, + ); + expect(await client.hget(key, field2, Decoder.Bytes)).toEqual( + valueEncoded, + ); + expect( + await client.hget(key, "nonExistingField", Decoder.Bytes), + ).toEqual(null); }, protocol); }, config.timeout, @@ -1765,6 +1802,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); + const key2 = uuidv4(); const field1 = uuidv4(); const field2 = uuidv4(); const fieldValueMap = { @@ -1772,11 +1810,25 @@ export function runBaseTests(config: { [field2]: "value2", }; + const value1Encoded = Buffer.from("value1"); + const value2Encoded = Buffer.from("value2"); + expect(await client.hset(key1, fieldValueMap)).toEqual(2); expect(await client.hvals(key1)).toEqual(["value1", "value2"]); expect(await client.hdel(key1, [field1])).toEqual(1); expect(await client.hvals(key1)).toEqual(["value2"]); expect(await client.hvals("nonExistingHash")).toEqual([]); + + //hvals with binary buffers + expect(await client.hset(key2, fieldValueMap)).toEqual(2); + expect(await client.hvals(key2, Decoder.Bytes)).toEqual([ + value1Encoded, + value2Encoded, + ]); + expect(await client.hdel(key2, [field1])).toEqual(1); + expect(await client.hvals(key2, Decoder.Bytes)).toEqual([ + value2Encoded, + ]); }, protocol); }, config.timeout, @@ -6026,7 +6078,9 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); const key2 = uuidv4(); + const key3 = uuidv4(); const value = uuidv4(); + const valueEncoded = Buffer.from(value); // Append on non-existing string(similar to SET) expect(await client.append(key1, value)).toBe(value.length); @@ -6038,6 +6092,17 @@ export function runBaseTests(config: { await expect(client.append(key2, "_")).rejects.toThrow( RequestError, ); + + // Key and value as buffers + expect(await client.append(key3, valueEncoded)).toBe( + value.length, + ); + expect(await client.append(key3, valueEncoded)).toBe( + valueEncoded.length * 2, + ); + expect(await client.get(key3, Decoder.Bytes)).toEqual( + Buffer.concat([valueEncoded, valueEncoded]), + ); }, protocol); }, config.timeout, From 0f00fd66474af12c7a92c3f2b3ed5946f1e88282 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Mon, 19 Aug 2024 00:22:39 +0000 Subject: [PATCH 185/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 10 +++++----- java/THIRD_PARTY_LICENSES_JAVA | 10 +++++----- node/THIRD_PARTY_LICENSES_NODE | 14 +++++++------- python/THIRD_PARTY_LICENSES_PYTHON | 14 +++++++------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index baf1a730e1..4de92d15d8 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -12361,7 +12361,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.155 +Package: libc:0.2.157 The following copyrights and licenses were found in the source code of this package: @@ -21689,7 +21689,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.207 +Package: serde:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -21918,7 +21918,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.207 +Package: serde_derive:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -23021,7 +23021,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.74 +Package: syn:2.0.75 The following copyrights and licenses were found in the source code of this package: @@ -25122,7 +25122,7 @@ the following restrictions: ---- -Package: tokio:1.39.2 +Package: tokio:1.39.3 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 f4fcb68d47..aaec2fa714 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -13256,7 +13256,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.155 +Package: libc:0.2.157 The following copyrights and licenses were found in the source code of this package: @@ -22584,7 +22584,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.207 +Package: serde:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -22813,7 +22813,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.207 +Package: serde_derive:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -23916,7 +23916,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.74 +Package: syn:2.0.75 The following copyrights and licenses were found in the source code of this package: @@ -26017,7 +26017,7 @@ the following restrictions: ---- -Package: tokio:1.39.2 +Package: tokio:1.39.3 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 28f95e8a6b..8e3c4aa182 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -12875,7 +12875,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.155 +Package: libc:0.2.157 The following copyrights and licenses were found in the source code of this package: @@ -23262,7 +23262,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.207 +Package: serde:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -23491,7 +23491,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.207 +Package: serde_derive:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -24594,7 +24594,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.74 +Package: syn:2.0.75 The following copyrights and licenses were found in the source code of this package: @@ -27153,7 +27153,7 @@ the following restrictions: ---- -Package: tokio:1.39.2 +Package: tokio:1.39.3 The following copyrights and licenses were found in the source code of this package: @@ -37493,7 +37493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: undici-types:6.13.0 +Package: undici-types:6.19.6 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.2.0 +Package: @types:node:22.4.0 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 98761ba6c4..eb74a0cbaa 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -13027,7 +13027,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.155 +Package: libc:0.2.157 The following copyrights and licenses were found in the source code of this package: @@ -23754,7 +23754,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.207 +Package: serde:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -23983,7 +23983,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.207 +Package: serde_derive:1.0.208 The following copyrights and licenses were found in the source code of this package: @@ -25086,7 +25086,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.74 +Package: syn:2.0.75 The following copyrights and licenses were found in the source code of this package: @@ -27411,7 +27411,7 @@ the following restrictions: ---- -Package: tokio:1.39.2 +Package: tokio:1.39.3 The following copyrights and licenses were found in the source code of this package: @@ -34930,7 +34930,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: cachetools:5.4.0 +Package: cachetools:5.5.0 The following copyrights and licenses were found in the source code of this package: @@ -35832,7 +35832,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: google-auth:2.33.0 +Package: google-auth:2.34.0 The following copyrights and licenses were found in the source code of this package: From 322d8d79bc76723074b92f72bfc980d5f57db461 Mon Sep 17 00:00:00 2001 From: janhavigupta007 <46344506+janhavigupta007@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:20:46 +0000 Subject: [PATCH 186/236] Go client: Adding a skeleton to create client connection and invoke SET and GET commands. (#1937) * Go: create client connection (#1303) * Go: Implement benchmarking (#1330) * Update DEVELOPER.md with instructions to extend LD_LIBRARY_PATH on ubuntu/centos * Go: Fix bug with -clients=all setting (#1339) * Go: Fixes a bug where the benchmarks were ran twice for go-redis when the -clients option was set to "all" * Go: update protobuf version to address dependabot alert (#1352) * Go client: Preparing the skeleton code to create the client connection and invoke set and get commands Signed-off-by: Janhavi Gupta --------- Signed-off-by: Janhavi Gupta Co-authored-by: Andrew Carbonetto Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> Co-authored-by: aaron-congo --- .github/workflows/go.yml | 32 +- benchmarks/install_and_test.sh | 25 +- benchmarks/utilities/csv_exporter.py | 2 +- go/.gitignore | 4 + go/Cargo.toml | 1 + go/DEVELOPER.md | 46 ++- go/Makefile | 15 +- go/api/base_client.go | 162 ++++++++ go/api/command_options.go | 71 ++++ go/api/commands.go | 52 +++ go/api/config.go | 97 ++--- go/api/config_test.go | 16 +- go/api/errors.go | 65 +++ go/api/glide_client.go | 43 ++ go/api/glide_cluster_client.go | 18 + go/api/response_handlers.go | 32 ++ go/benchmarks/benchmarking.go | 460 +++++++++++++++++++++ go/benchmarks/glide_benchmark_client.go | 54 +++ go/benchmarks/go.mod | 16 + go/benchmarks/go.sum | 15 + go/benchmarks/go_redis_benchmark_client.go | 71 ++++ go/benchmarks/main.go | 195 +++++++++ go/examples/main.go | 56 +++ go/go.mod | 1 + go/go.sum | 2 + go/integTest/connection_test.go | 55 +++ go/integTest/glide_test_suite_test.go | 192 +++++++++ go/integTest/shared_commands_test.go | 172 ++++++++ go/integTest/standalone_commands_test.go | 74 ++++ go/src/lib.rs | 242 ++++++++++- 30 files changed, 2193 insertions(+), 93 deletions(-) create mode 100644 go/api/base_client.go create mode 100644 go/api/command_options.go create mode 100644 go/api/commands.go create mode 100644 go/api/errors.go create mode 100644 go/api/glide_client.go create mode 100644 go/api/glide_cluster_client.go create mode 100644 go/api/response_handlers.go create mode 100644 go/benchmarks/benchmarking.go create mode 100644 go/benchmarks/glide_benchmark_client.go create mode 100644 go/benchmarks/go.mod create mode 100644 go/benchmarks/go.sum create mode 100644 go/benchmarks/go_redis_benchmark_client.go create mode 100644 go/benchmarks/main.go create mode 100644 go/examples/main.go create mode 100644 go/integTest/connection_test.go create mode 100644 go/integTest/glide_test_suite_test.go create mode 100644 go/integTest/shared_commands_test.go create mode 100644 go/integTest/standalone_commands_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a7654519d7..7f6bd60a4a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -106,7 +106,22 @@ jobs: - name: Run tests working-directory: ./go - run: make test + run: | + make test + + - uses: ./.github/workflows/test-benchmark + with: + language-flag: -go + + - name: Upload logs and reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: reports-go-${{ matrix.go }}-redis-${{ matrix.redis }}-${{ matrix.os }} + path: | + utils/clusters/** + benchmarks/results/** build-amazonlinux-latest: if: github.repository_owner == 'valkey-io' @@ -158,6 +173,9 @@ jobs: working-directory: ./go run: make install-tools-go${{ matrix.go }} + - name: Set LD_LIBRARY_PATH + run: echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/" >> $GITHUB_ENV + - name: Build client working-directory: ./go run: make build @@ -168,7 +186,17 @@ jobs: - name: Run tests working-directory: ./go - run: make test + run: | + make test + + - name: Upload cluster manager logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: cluster-manager-logs-${{ matrix.go }}-redis-6-amazonlinux + path: | + utils/clusters/** lint-rust: timeout-minutes: 15 diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index 5fb83801dc..ae50fb5e61 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -25,6 +25,7 @@ runPython=0 runNode=0 runCsharp=0 runJava=0 +runGo=0 runRust=0 concurrentTasks="1 10 100 1000" dataSize="100 4000" @@ -76,6 +77,12 @@ function runJavaBenchmark(){ ./gradlew :benchmarks:run --args="-resultsFile \"${BENCH_FOLDER}/$1\" --dataSize \"$2\" --concurrentTasks \"$concurrentTasks\" --clients \"$chosenClients\" --host $host $portFlag --clientCount \"$clientCount\" $tlsFlag $clusterFlag $minimalFlag" } +function runGoBenchmark(){ + cd ${BENCH_FOLDER}/../go/benchmarks + export LD_LIBRARY_PATH=${BENCH_FOLDER}/../go/target/release:$LD_LIBRARY_PATH + go run . -resultsFile ${BENCH_FOLDER}/$1 -dataSize $2 -concurrentTasks $concurrentTasks -clients $chosenClients -host $host $portFlag -clientCount $clientCount $tlsFlag $clusterFlag $minimalFlag +} + function runRustBenchmark(){ rustConcurrentTasks= for value in $concurrentTasks @@ -115,7 +122,7 @@ function resultFileName() { function Help() { echo Running the script without any arguments runs all benchmarks. - echo Pass -node, -csharp, -python, -java as arguments in order to run the node, csharp, python, or java benchmarks accordingly. + echo Pass -node, -csharp, -python, -java, -go as arguments in order to run the node, csharp, python, java, or go benchmarks accordingly. echo Multiple such flags can be passed. echo Pass -no-csv to skip analysis of the results. echo @@ -204,6 +211,15 @@ do runJava=1 chosenClients="Jedis" ;; + -go) + runAllBenchmarks=0 + runGo=1 + ;; + -go-redis) + runAllBenchmarks=0 + runGo=1 + chosenClients="go-redis" + ;; -csharp) runAllBenchmarks=0 runCsharp=1 @@ -274,6 +290,13 @@ do runJavaBenchmark $javaResults $currentDataSize fi + if [ $runAllBenchmarks == 1 ] || [ $runGo == 1 ]; + then + goResults=$(resultFileName go $currentDataSize) + resultFiles+=$goResults" " + runGoBenchmark $goResults $currentDataSize + fi + if [ $runAllBenchmarks == 1 ] || [ $runRust == 1 ]; then rustResults=$(resultFileName rust $currentDataSize) diff --git a/benchmarks/utilities/csv_exporter.py b/benchmarks/utilities/csv_exporter.py index 2841e867f6..14004d0532 100755 --- a/benchmarks/utilities/csv_exporter.py +++ b/benchmarks/utilities/csv_exporter.py @@ -43,7 +43,7 @@ json_file_name = os.path.basename(json_file_full_path) - languages = ["csharp", "node", "python", "rust", "java"] + languages = ["csharp", "node", "python", "rust", "java", "go"] language = next( (language for language in languages if language in json_file_name), None ) diff --git a/go/.gitignore b/go/.gitignore index cbb173b891..93cec72cd1 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -5,3 +5,7 @@ reports # cbindgen generated header file lib.h + +# benchmarking results +benchmarks/results/** +benchmarks/gobenchmarks.json diff --git a/go/Cargo.toml b/go/Cargo.toml index 6d6c4ecb15..62872578da 100644 --- a/go/Cargo.toml +++ b/go/Cargo.toml @@ -13,6 +13,7 @@ redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp" glide-core = { path = "../glide-core", features = ["socket-layer"] } tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread", "time"] } protobuf = { version = "3.3.0", features = [] } +derivative = "2.2.0" [profile.release] lto = true diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 22ec84c264..023828a0cf 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -1,12 +1,12 @@ # Developer Guide -This document describes how to set up your development environment to build and test the GLIDE for Redis Go wrapper. +This document describes how to set up your development environment to build and test the Valkey GLIDE Go wrapper. ### Development Overview We're excited to share that the GLIDE Go client is currently in development! However, it's important to note that this client is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue to develop this Go wrapper. -The GLIDE for Redis Go wrapper consists of both Go and Rust code. The Go and Rust components communicate in two ways: +The Valkey GLIDE Go wrapper consists of both Go and Rust code. The Go and Rust components communicate in two ways: 1. Using the [protobuf](https://github.com/protocolbuffers/protobuf) protocol. 2. Using shared C objects. [cgo](https://pkg.go.dev/cmd/cgo) is used to interact with the C objects from Go code. @@ -25,11 +25,10 @@ Software Dependencies - openssl - openssl-dev - rustup -- redis -**Redis installation** +**Valkey installation** -To install redis-server and redis-cli on your host, follow the [Redis Installation Guide](https://redis.io/docs/install/install-redis/). +To install valkey-server and valkey-cli on your host, follow the [Valkey Installation Guide](https://github.com/valkey-io/valkey). **Dependencies installation for Ubuntu** @@ -102,7 +101,8 @@ Before starting this step, make sure you've installed all software requirements. 1. Clone the repository: ```bash - git clone https://github.com/valkey-io/valkey-glide.git + VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch + git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git cd valkey-glide ``` 2. Initialize git submodule: @@ -114,20 +114,28 @@ Before starting this step, make sure you've installed all software requirements. cd go make install-build-tools ``` -4. Build the Go wrapper: +4. If on CentOS or Ubuntu, add the glide-rs library to LD_LIBRARY_PATH: + ```bash + # Replace "" with the path to the valkey-glide root, eg "$HOME/Projects/valkey-glide" + GLIDE_ROOT_FOLDER_PATH= + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GLIDE_ROOT_FOLDER_PATH/go/target/release/deps/ + ``` +5. Build the Go wrapper: ```bash make build ``` -5. Run tests: - 1. Ensure that you have installed redis-server and redis-cli on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). +6. Run tests: + 1. Ensure that you have installed valkey-server and valkey-cli on your host. You can find the Valkey installation guide at the following link: [Valkey Installation Guide](https://github.com/valkey-io/valkey). 2. Execute the following command from the go folder: ```bash go test -race ./... ``` -6. Install Go development tools with: - +7. Install Go development tools with: ```bash + # For go1.22: make install-dev-tools + # For go1.18: + make install-dev-tools-go1.18 ``` ### Test @@ -191,7 +199,11 @@ Run from the main `/go` folder 1. Go ```bash + # For go1.22: make install-dev-tools + # For go1.18: + make install-dev-tools-go1.18 + make lint ``` 2. Rust @@ -211,6 +223,18 @@ Run from the main `/go` folder make format ``` +### Benchmarks + +To run the benchmarks, ensure you have followed the [build and installation steps](#building-and-installation-steps) (the tests do not have to be run). Then execute the following: + +```bash +cd go/benchmarks +# To see a list of available options and their defaults: +go run . -help +# An example command setting various options: +go run . -resultsFile gobenchmarks.json -dataSize "100 1000" -concurrentTasks "10 100" -clients all -host localhost -port 6379 -clientCount "1 5" -tls +``` + ### Recommended extensions for VS Code - [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go) diff --git a/go/Makefile b/go/Makefile index a628643d1c..f5bfb7a0ca 100644 --- a/go/Makefile +++ b/go/Makefile @@ -1,19 +1,22 @@ install-build-tools: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0 + cargo install cbindgen -install-dev-tools-go1.18.10: +install-dev-tools-go1.18: go install github.com/vakenbolt/go-test-report@v0.9.3 go install mvdan.cc/gofumpt@v0.4.0 go install github.com/segmentio/golines@v0.11.0 go install honnef.co/go/tools/cmd/staticcheck@v0.3.3 - cargo install cbindgen -install-dev-tools-go1.22.0: +install-dev-tools-go1.18.10: install-dev-tools-go1.18 + +install-dev-tools-go1.22: go install github.com/vakenbolt/go-test-report@v0.9.3 go install mvdan.cc/gofumpt@v0.6.0 go install github.com/segmentio/golines@v0.12.2 go install honnef.co/go/tools/cmd/staticcheck@v0.4.6 - cargo install cbindgen + +install-dev-tools-go1.22.0: install-dev-tools-go1.22 install-dev-tools: install-dev-tools-go1.22.0 @@ -55,8 +58,12 @@ format: golines -w --shorten-comments -m 127 . test: + LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ go test -v -race ./... +# Note: this task is no longer run by CI because: +# - build failures that occur while running the task can be hidden by the task; CI still reports success in these scenarios. +# - there is not a good way to both generate a test report and log the test outcomes to GH actions. test-and-report: mkdir -p reports go test -v -race ./... -json | go-test-report -o reports/test-report.html diff --git a/go/api/base_client.go b/go/api/base_client.go new file mode 100644 index 0000000000..48da19cb8f --- /dev/null +++ b/go/api/base_client.go @@ -0,0 +1,162 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +// +// void successCallback(void *channelPtr, struct CommandResponse *message); +// void failureCallback(void *channelPtr, char *errMessage, RequestErrorType errType); +import "C" + +import ( + "unsafe" + + "github.com/valkey-io/valkey-glide/go/glide/protobuf" + "google.golang.org/protobuf/proto" +) + +// BaseClient defines an interface for methods common to both [GlideClient] and [GlideClusterClient]. +type BaseClient interface { + StringCommands + + // Close terminates the client by closing all associated resources. + Close() +} + +const OK = "OK" + +type payload struct { + value *C.struct_CommandResponse + error error +} + +//export successCallback +func successCallback(channelPtr unsafe.Pointer, cResponse *C.struct_CommandResponse) { + response := cResponse + resultChannel := *(*chan payload)(channelPtr) + resultChannel <- payload{value: response, error: nil} +} + +//export failureCallback +func failureCallback(channelPtr unsafe.Pointer, cErrorMessage *C.char, cErrorType C.RequestErrorType) { + resultChannel := *(*chan payload)(channelPtr) + resultChannel <- payload{value: nil, error: goError(cErrorType, cErrorMessage)} +} + +type clientConfiguration interface { + toProtobuf() *protobuf.ConnectionRequest +} + +type baseClient struct { + coreClient unsafe.Pointer +} + +// Creates a connection by invoking the `create_client` function from Rust library via FFI. +// Passes the pointers to callback functions which will be invoked when the command succeeds or fails. +// Once the connection is established, this function invokes `free_connection_response` exposed by rust library to free the +// connection_response to avoid any memory leaks. +func createClient(config clientConfiguration) (*baseClient, error) { + request := config.toProtobuf() + msg, err := proto.Marshal(request) + if err != nil { + return nil, err + } + + byteCount := len(msg) + requestBytes := C.CBytes(msg) + cResponse := (*C.struct_ConnectionResponse)( + C.create_client( + (*C.uchar)(requestBytes), + C.uintptr_t(byteCount), + (C.SuccessCallback)(unsafe.Pointer(C.successCallback)), + (C.FailureCallback)(unsafe.Pointer(C.failureCallback)), + ), + ) + defer C.free_connection_response(cResponse) + + cErr := cResponse.connection_error_message + if cErr != nil { + message := C.GoString(cErr) + return nil, &ConnectionError{message} + } + + return &baseClient{cResponse.conn_ptr}, nil +} + +// Close terminates the client by closing all associated resources. +func (client *baseClient) Close() { + if client.coreClient == nil { + return + } + + C.close_client(client.coreClient) + client.coreClient = nil +} + +func (client *baseClient) executeCommand(requestType C.RequestType, args []string) (*C.struct_CommandResponse, error) { + if client.coreClient == nil { + return nil, &ClosingError{"ExecuteCommand failed. The client is closed."} + } + + cArgs, argLengths := toCStrings(args) + defer freeCStrings(cArgs) + + resultChannel := make(chan payload) + resultChannelPtr := uintptr(unsafe.Pointer(&resultChannel)) + + C.command( + client.coreClient, + C.uintptr_t(resultChannelPtr), + uint32(requestType), + C.size_t(len(args)), + &cArgs[0], + &argLengths[0], + ) + payload := <-resultChannel + if payload.error != nil { + return nil, payload.error + } + return payload.value, nil +} + +// TODO: Handle passing the arguments as strings without assuming null termination assumption. +func toCStrings(args []string) ([]*C.char, []C.ulong) { + cStrings := make([]*C.char, len(args)) + stringLengths := make([]C.ulong, len(args)) + for i, str := range args { + cStrings[i] = C.CString(str) + stringLengths[i] = C.size_t(len(str)) + } + return cStrings, stringLengths +} + +func freeCStrings(cArgs []*C.char) { + for _, arg := range cArgs { + C.free(unsafe.Pointer(arg)) + } +} + +func (client *baseClient) Set(key string, value string) (string, error) { + result, err := client.executeCommand(C.Set, []string{key, value}) + if err != nil { + return "", err + } + return handleStringResponse(result), nil +} + +func (client *baseClient) SetWithOptions(key string, value string, options *SetOptions) (string, error) { + result, err := client.executeCommand(C.Set, append([]string{key, value}, options.toArgs()...)) + if err != nil { + return "", err + } + return handleStringOrNullResponse(result), nil +} + +func (client *baseClient) Get(key string) (string, error) { + result, err := client.executeCommand(C.Get, []string{key}) + if err != nil { + return "", err + } + return handleStringOrNullResponse(result), nil +} diff --git a/go/api/command_options.go b/go/api/command_options.go new file mode 100644 index 0000000000..d5bc66498d --- /dev/null +++ b/go/api/command_options.go @@ -0,0 +1,71 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "strconv" + +// SetOptions represents optional arguments for the [api.StringCommands.SetWithOptions] command. +// +// See [valkey.io] +// +// [valkey.io]: https://valkey.io/commands/set/ +type SetOptions struct { + // If ConditionalSet is not set the value will be set regardless of prior value existence. If value isn't set because of + // the condition, [api.StringCommands.SetWithOptions] will return a zero-value string (""). + ConditionalSet ConditionalSet + // Set command to return the old value stored at the given key, or a zero-value string ("") if the key did not exist. An + // error is returned and [api.StringCommands.SetWithOptions] is aborted if the value stored at key is not a string. + // Equivalent to GET in the valkey API. + ReturnOldValue bool + // If not set, no expiry time will be set for the value. + Expiry *Expiry +} + +func (opts *SetOptions) toArgs() []string { + args := []string{} + if opts.ConditionalSet != "" { + args = append(args, string(opts.ConditionalSet)) + } + + if opts.ReturnOldValue { + args = append(args, returnOldValue) + } + + if opts.Expiry != nil { + args = append(args, string(opts.Expiry.Type)) + if opts.Expiry.Type != KeepExisting { + args = append(args, strconv.FormatUint(opts.Expiry.Count, 10)) + } + } + + return args +} + +const returnOldValue = "GET" + +// A ConditionalSet defines whether a new value should be set or not. +type ConditionalSet string + +const ( + // OnlyIfExists only sets the key if it already exists. Equivalent to "XX" in the valkey API. + OnlyIfExists ConditionalSet = "XX" + // OnlyIfDoesNotExist only sets the key if it does not already exist. Equivalent to "NX" in the valkey API. + OnlyIfDoesNotExist ConditionalSet = "NX" +) + +// Expiry is used to configure the lifetime of a value. +type Expiry struct { + Type ExpiryType + Count uint64 +} + +// An ExpiryType is used to configure the type of expiration for a value. +type ExpiryType string + +const ( + KeepExisting ExpiryType = "KEEPTTL" // keep the existing expiration of the value + Seconds ExpiryType = "EX" // expire the value after [api.Expiry.Count] seconds + Milliseconds ExpiryType = "PX" // expire the value after [api.Expiry.Count] milliseconds + UnixSeconds ExpiryType = "EXAT" // expire the value after the Unix time specified by [api.Expiry.Count], in seconds + UnixMilliseconds ExpiryType = "PXAT" // expire the value after the Unix time specified by [api.Expiry.Count], in milliseconds +) diff --git a/go/api/commands.go b/go/api/commands.go new file mode 100644 index 0000000000..c19f318df1 --- /dev/null +++ b/go/api/commands.go @@ -0,0 +1,52 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// StringCommands defines an interface for the "String Commands" group of commands for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/?group=string +type StringCommands interface { + // Set the given key with the given value. The return value is a response from Valkey containing the string "OK". + // + // See [valkey.io] for details. + // + // For example: + // + // result, err := client.Set("key", "value") + // + // [valkey.io]: https://valkey.io/commands/set/ + Set(key string, value string) (string, error) + + // SetWithOptions sets the given key with the given value using the given options. The return value is dependent on the + // passed options. If the value is successfully set, "OK" is returned. If value isn't set because of [OnlyIfExists] or + // [OnlyIfDoesNotExist] conditions, an empty string is returned (""). If [SetOptions#ReturnOldValue] is set, the old + // value is returned. + // + // See [valkey.io] for details. + // + // For example: + // + // result, err := client.SetWithOptions("key", "value", &api.SetOptions{ + // ConditionalSet: api.OnlyIfExists, + // Expiry: &api.Expiry{ + // Type: api.Seconds, + // Count: uint64(5), + // }, + // }) + // + // [valkey.io]: https://valkey.io/commands/set/ + SetWithOptions(key string, value string, options *SetOptions) (string, error) + + // Get string value associated with the given key, or an empty string is returned ("") if no such value exists + // + // See [valkey.io] for details. + // + // For example: + // + // result, err := client.Get("key") + // + // [valkey.io]: https://valkey.io/commands/get/ + Get(key string) (string, error) +} diff --git a/go/api/config.go b/go/api/config.go index cc20d5e7c8..76b3c14fbf 100644 --- a/go/api/config.go +++ b/go/api/config.go @@ -27,27 +27,27 @@ func (addr *NodeAddress) toProtobuf() *protobuf.NodeAddress { return &protobuf.NodeAddress{Host: addr.Host, Port: uint32(addr.Port)} } -// RedisCredentials represents the credentials for connecting to a Redis server. -type RedisCredentials struct { - // The username that will be used for authenticating connections to the Redis servers. If not supplied, "default" +// ServerCredentials represents the credentials for connecting to servers. +type ServerCredentials struct { + // The username that will be used for authenticating connections to the servers. If not supplied, "default" // will be used. username string - // The password that will be used for authenticating connections to the Redis servers. + // The password that will be used for authenticating connections to the servers. password string } -// NewRedisCredentials returns a [RedisCredentials] struct with the given username and password. -func NewRedisCredentials(username string, password string) *RedisCredentials { - return &RedisCredentials{username, password} +// NewServerCredentials returns a [ServerCredentials] struct with the given username and password. +func NewServerCredentials(username string, password string) *ServerCredentials { + return &ServerCredentials{username, password} } -// NewRedisCredentialsWithDefaultUsername returns a [RedisCredentials] struct with a default username of "default" and the +// NewServerCredentialsWithDefaultUsername returns a [ServerCredentials] struct with a default username of "default" and the // given password. -func NewRedisCredentialsWithDefaultUsername(password string) *RedisCredentials { - return &RedisCredentials{password: password} +func NewServerCredentialsWithDefaultUsername(password string) *ServerCredentials { + return &ServerCredentials{password: password} } -func (creds *RedisCredentials) toProtobuf() *protobuf.AuthenticationInfo { +func (creds *ServerCredentials) toProtobuf() *protobuf.AuthenticationInfo { return &protobuf.AuthenticationInfo{Username: creds.username, Password: creds.password} } @@ -73,7 +73,7 @@ func mapReadFrom(readFrom ReadFrom) protobuf.ReadFrom { type baseClientConfiguration struct { addresses []NodeAddress useTLS bool - credentials *RedisCredentials + credentials *ServerCredentials readFrom ReadFrom requestTimeout int clientName string @@ -140,21 +140,20 @@ func (strategy *BackoffStrategy) toProtobuf() *protobuf.ConnectionRetryStrategy } } -// RedisClientConfiguration represents the configuration settings for a Standalone Redis client. baseClientConfiguration is an -// embedded struct that contains shared settings for standalone and cluster clients. -type RedisClientConfiguration struct { +// GlideClientConfiguration represents the configuration settings for a Standalone client. +type GlideClientConfiguration struct { baseClientConfiguration reconnectStrategy *BackoffStrategy databaseId int } -// NewRedisClientConfiguration returns a [RedisClientConfiguration] with default configuration settings. For further -// configuration, use the [RedisClientConfiguration] With* methods. -func NewRedisClientConfiguration() *RedisClientConfiguration { - return &RedisClientConfiguration{} +// NewGlideClientConfiguration returns a [GlideClientConfiguration] with default configuration settings. For further +// configuration, use the [GlideClientConfiguration] With* methods. +func NewGlideClientConfiguration() *GlideClientConfiguration { + return &GlideClientConfiguration{} } -func (config *RedisClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { +func (config *GlideClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { request := config.baseClientConfiguration.toProtobuf() request.ClusterModeEnabled = false if config.reconnectStrategy != nil { @@ -171,14 +170,16 @@ func (config *RedisClientConfiguration) toProtobuf() *protobuf.ConnectionRequest // WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be // called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as // the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose -// addresses were provided will be used by the client. For example: +// addresses were provided will be used by the client. // -// config := NewRedisClientConfiguration(). +// For example: +// +// config := NewGlideClientConfiguration(). // WithAddress(&NodeAddress{ // Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). // WithAddress(&NodeAddress{ // Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) -func (config *RedisClientConfiguration) WithAddress(address *NodeAddress) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithAddress(address *NodeAddress) *GlideClientConfiguration { config.addresses = append(config.addresses, *address) return config } @@ -186,20 +187,20 @@ func (config *RedisClientConfiguration) WithAddress(address *NodeAddress) *Redis // WithUseTLS configures the TLS settings for this configuration. Set to true if communication with the cluster should use // Transport Level Security. This setting should match the TLS configuration of the server/cluster, otherwise the connection // attempt will fail. -func (config *RedisClientConfiguration) WithUseTLS(useTLS bool) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithUseTLS(useTLS bool) *GlideClientConfiguration { config.useTLS = useTLS return config } // WithCredentials sets the credentials for the authentication process. If none are set, the client will not authenticate // itself with the server. -func (config *RedisClientConfiguration) WithCredentials(credentials *RedisCredentials) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithCredentials(credentials *ServerCredentials) *GlideClientConfiguration { config.credentials = credentials return config } // WithReadFrom sets the client's [ReadFrom] strategy. If not set, [Primary] will be used. -func (config *RedisClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithReadFrom(readFrom ReadFrom) *GlideClientConfiguration { config.readFrom = readFrom return config } @@ -208,47 +209,47 @@ func (config *RedisClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisCl // encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the // specified timeout is exceeded for a pending request, it will result in a timeout error. If not set, a default value will be // used. -func (config *RedisClientConfiguration) WithRequestTimeout(requestTimeout int) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithRequestTimeout(requestTimeout int) *GlideClientConfiguration { config.requestTimeout = requestTimeout return config } // WithClientName sets the client name to be used for the client. Will be used with CLIENT SETNAME command during connection // establishment. -func (config *RedisClientConfiguration) WithClientName(clientName string) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithClientName(clientName string) *GlideClientConfiguration { config.clientName = clientName return config } // WithReconnectStrategy sets the [BackoffStrategy] used to determine how and when to reconnect, in case of connection // failures. If not set, a default backoff strategy will be used. -func (config *RedisClientConfiguration) WithReconnectStrategy(strategy *BackoffStrategy) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithReconnectStrategy(strategy *BackoffStrategy) *GlideClientConfiguration { config.reconnectStrategy = strategy return config } // WithDatabaseId sets the index of the logical database to connect to. -func (config *RedisClientConfiguration) WithDatabaseId(id int) *RedisClientConfiguration { +func (config *GlideClientConfiguration) WithDatabaseId(id int) *GlideClientConfiguration { config.databaseId = id return config } -// RedisClusterClientConfiguration represents the configuration settings for a Cluster Redis client. +// GlideClusterClientConfiguration represents the configuration settings for a Cluster Glide client. // Note: Currently, the reconnection strategy in cluster mode is not configurable, and exponential backoff with fixed values is // used. -type RedisClusterClientConfiguration struct { +type GlideClusterClientConfiguration struct { baseClientConfiguration } -// NewRedisClusterClientConfiguration returns a [RedisClusterClientConfiguration] with default configuration settings. For -// further configuration, use the [RedisClientConfiguration] With* methods. -func NewRedisClusterClientConfiguration() *RedisClusterClientConfiguration { - return &RedisClusterClientConfiguration{ +// NewGlideClusterClientConfiguration returns a [GlideClusterClientConfiguration] with default configuration settings. For +// further configuration, use the [GlideClientConfiguration] With* methods. +func NewGlideClusterClientConfiguration() *GlideClusterClientConfiguration { + return &GlideClusterClientConfiguration{ baseClientConfiguration: baseClientConfiguration{}, } } -func (config *RedisClusterClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { +func (config *GlideClusterClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { request := config.baseClientConfiguration.toProtobuf() request.ClusterModeEnabled = true return request @@ -257,14 +258,16 @@ func (config *RedisClusterClientConfiguration) toProtobuf() *protobuf.Connection // WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be // called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as // the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose -// addresses were provided will be used by the client. For example: +// addresses were provided will be used by the client. +// +// For example: // -// config := NewRedisClusterClientConfiguration(). +// config := NewGlideClusterClientConfiguration(). // WithAddress(&NodeAddress{ // Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). // WithAddress(&NodeAddress{ // Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) -func (config *RedisClusterClientConfiguration) WithAddress(address *NodeAddress) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithAddress(address *NodeAddress) *GlideClusterClientConfiguration { config.addresses = append(config.addresses, *address) return config } @@ -272,22 +275,22 @@ func (config *RedisClusterClientConfiguration) WithAddress(address *NodeAddress) // WithUseTLS configures the TLS settings for this configuration. Set to true if communication with the cluster should use // Transport Level Security. This setting should match the TLS configuration of the server/cluster, otherwise the connection // attempt will fail. -func (config *RedisClusterClientConfiguration) WithUseTLS(useTLS bool) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithUseTLS(useTLS bool) *GlideClusterClientConfiguration { config.useTLS = useTLS return config } // WithCredentials sets the credentials for the authentication process. If none are set, the client will not authenticate // itself with the server. -func (config *RedisClusterClientConfiguration) WithCredentials( - credentials *RedisCredentials, -) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithCredentials( + credentials *ServerCredentials, +) *GlideClusterClientConfiguration { config.credentials = credentials return config } // WithReadFrom sets the client's [ReadFrom] strategy. If not set, [Primary] will be used. -func (config *RedisClusterClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithReadFrom(readFrom ReadFrom) *GlideClusterClientConfiguration { config.readFrom = readFrom return config } @@ -296,14 +299,14 @@ func (config *RedisClusterClientConfiguration) WithReadFrom(readFrom ReadFrom) * // encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the // specified timeout is exceeded for a pending request, it will result in a timeout error. If not set, a default value will be // used. -func (config *RedisClusterClientConfiguration) WithRequestTimeout(requestTimeout int) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithRequestTimeout(requestTimeout int) *GlideClusterClientConfiguration { config.requestTimeout = requestTimeout return config } // WithClientName sets the client name to be used for the client. Will be used with CLIENT SETNAME command during connection // establishment. -func (config *RedisClusterClientConfiguration) WithClientName(clientName string) *RedisClusterClientConfiguration { +func (config *GlideClusterClientConfiguration) WithClientName(clientName string) *GlideClusterClientConfiguration { config.clientName = clientName return config } diff --git a/go/api/config_test.go b/go/api/config_test.go index 44e6530d9d..dcb778798e 100644 --- a/go/api/config_test.go +++ b/go/api/config_test.go @@ -11,7 +11,7 @@ import ( ) func TestDefaultStandaloneConfig(t *testing.T) { - config := NewRedisClientConfiguration() + config := NewGlideClientConfiguration() expected := &protobuf.ConnectionRequest{ TlsMode: protobuf.TlsMode_NoTls, ClusterModeEnabled: false, @@ -24,7 +24,7 @@ func TestDefaultStandaloneConfig(t *testing.T) { } func TestDefaultClusterConfig(t *testing.T) { - config := NewRedisClusterClientConfiguration() + config := NewGlideClusterClientConfiguration() expected := &protobuf.ConnectionRequest{ TlsMode: protobuf.TlsMode_NoTls, ClusterModeEnabled: true, @@ -46,10 +46,10 @@ func TestConfig_allFieldsSet(t *testing.T) { retries, factor, base := 5, 10, 50 databaseId := 1 - config := NewRedisClientConfiguration(). + config := NewGlideClientConfiguration(). WithUseTLS(true). WithReadFrom(PreferReplica). - WithCredentials(NewRedisCredentials(username, password)). + WithCredentials(NewServerCredentials(username, password)). WithRequestTimeout(timeout). WithClientName(clientName). WithReconnectStrategy(NewBackoffStrategy(retries, factor, base)). @@ -104,17 +104,17 @@ func TestNodeAddress(t *testing.T) { } } -func TestRedisCredentials(t *testing.T) { +func TestServerCredentials(t *testing.T) { parameters := []struct { - input *RedisCredentials + input *ServerCredentials expected *protobuf.AuthenticationInfo }{ { - NewRedisCredentials("username", "password"), + NewServerCredentials("username", "password"), &protobuf.AuthenticationInfo{Username: "username", Password: "password"}, }, { - NewRedisCredentialsWithDefaultUsername("password"), + NewServerCredentialsWithDefaultUsername("password"), &protobuf.AuthenticationInfo{Password: "password"}, }, } diff --git a/go/api/errors.go b/go/api/errors.go new file mode 100644 index 0000000000..4fa4fad92a --- /dev/null +++ b/go/api/errors.go @@ -0,0 +1,65 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + +// ConnectionError is a client error that occurs when there is an error while connecting or when a connection +// disconnects. +type ConnectionError struct { + msg string +} + +func (e *ConnectionError) Error() string { return e.msg } + +// RequestError is a client error that occurs when an error is reported during a request. +type RequestError struct { + msg string +} + +func (e *RequestError) Error() string { return e.msg } + +// ExecAbortError is a client error that occurs when a transaction is aborted. +type ExecAbortError struct { + msg string +} + +func (e *ExecAbortError) Error() string { return e.msg } + +// TimeoutError is a client error that occurs when a request times out. +type TimeoutError struct { + msg string +} + +func (e *TimeoutError) Error() string { return e.msg } + +// DisconnectError is a client error that indicates a connection problem between Glide and server. +type DisconnectError struct { + msg string +} + +func (e *DisconnectError) Error() string { return e.msg } + +// ClosingError is a client error that indicates that the client has closed and is no longer usable. +type ClosingError struct { + msg string +} + +func (e *ClosingError) Error() string { return e.msg } + +func goError(cErrorType C.RequestErrorType, cErrorMessage *C.char) error { + defer C.free_error_message(cErrorMessage) + msg := C.GoString(cErrorMessage) + switch cErrorType { + case C.ExecAbort: + return &ExecAbortError{msg} + case C.Timeout: + return &TimeoutError{msg} + case C.Disconnect: + return &DisconnectError{msg} + default: + return &RequestError{msg} + } +} diff --git a/go/api/glide_client.go b/go/api/glide_client.go new file mode 100644 index 0000000000..7dfb23cc02 --- /dev/null +++ b/go/api/glide_client.go @@ -0,0 +1,43 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + +// GlideClient is a client used for connection in Standalone mode. +type GlideClient struct { + *baseClient +} + +// NewGlideClient creates a [GlideClient] in standalone mode using the given [GlideClientConfiguration]. +func NewGlideClient(config *GlideClientConfiguration) (*GlideClient, error) { + client, err := createClient(config) + if err != nil { + return nil, err + } + + return &GlideClient{client}, nil +} + +// CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, including +// the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed +// command. +// +// This function should only be used for single-response commands. Commands that don't return complete response and awaits +// (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's +// behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. +// +// For example, to return a list of all pub/sub clients: +// +// client.CustomCommand([]string{"CLIENT", "LIST","TYPE", "PUBSUB"}) +// +// TODO: Add support for complex return types. +func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { + res, err := client.executeCommand(C.CustomCommand, args) + if err != nil { + return nil, err + } + return handleStringOrNullResponse(res), nil +} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go new file mode 100644 index 0000000000..7ab186d7c2 --- /dev/null +++ b/go/api/glide_cluster_client.go @@ -0,0 +1,18 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// GlideClusterClient is a client used for connection in cluster mode. +type GlideClusterClient struct { + *baseClient +} + +// NewGlideClusterClient creates a [GlideClusterClient] in cluster mode using the given [GlideClusterClientConfiguration]. +func NewGlideClusterClient(config *GlideClusterClientConfiguration) (*GlideClusterClient, error) { + client, err := createClient(config) + if err != nil { + return nil, err + } + + return &GlideClusterClient{client}, nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go new file mode 100644 index 0000000000..524c7e057a --- /dev/null +++ b/go/api/response_handlers.go @@ -0,0 +1,32 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + +import ( + "unsafe" +) + +func convertCharArrayToString(arr *C.char, length C.long) string { + if arr == nil { + return "" + } + byteSlice := C.GoBytes(unsafe.Pointer(arr), C.int(int64(length))) + // Create Go string from byte slice (preserving null characters) + return string(byteSlice) +} + +func handleStringResponse(response *C.struct_CommandResponse) string { + defer C.free_command_response(response) + return convertCharArrayToString(response.string_value, response.string_value_len) +} + +func handleStringOrNullResponse(response *C.struct_CommandResponse) string { + if response == nil { + return "" + } + return handleStringResponse(response) +} diff --git a/go/benchmarks/benchmarking.go b/go/benchmarks/benchmarking.go new file mode 100644 index 0000000000..68061bd1b8 --- /dev/null +++ b/go/benchmarks/benchmarking.go @@ -0,0 +1,460 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package main + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "log" + "math" + "math/big" + "os" + "sort" + "strings" + "time" +) + +type connectionSettings struct { + host string + port int + useTLS bool + clusterModeEnabled bool +} + +func runBenchmarks(runConfig *runConfiguration) error { + connSettings := &connectionSettings{ + host: runConfig.host, + port: runConfig.port, + useTLS: runConfig.tls, + clusterModeEnabled: runConfig.clusterModeEnabled, + } + + err := executeBenchmarks(runConfig, connSettings) + if err != nil { + return err + } + + if runConfig.resultsFile != os.Stdout { + return writeResults(runConfig.resultsFile) + } + + return nil +} + +type benchmarkConfig struct { + clientName string + numConcurrentTasks int + clientCount int + dataSize int + minimal bool + connectionSettings *connectionSettings + resultsFile *os.File +} + +func executeBenchmarks(runConfig *runConfiguration, connectionSettings *connectionSettings) error { + var benchmarkConfigs []benchmarkConfig + for _, clientName := range runConfig.clientNames { + for _, numConcurrentTasks := range runConfig.concurrentTasks { + for _, clientCount := range runConfig.clientCount { + for _, dataSize := range runConfig.dataSize { + benchmarkConfig := benchmarkConfig{ + clientName: clientName, + numConcurrentTasks: numConcurrentTasks, + clientCount: clientCount, + dataSize: dataSize, + minimal: runConfig.minimal, + connectionSettings: connectionSettings, + resultsFile: runConfig.resultsFile, + } + + benchmarkConfigs = append(benchmarkConfigs, benchmarkConfig) + } + } + } + } + + for _, config := range benchmarkConfigs { + err := runSingleBenchmark(&config) + if err != nil { + return err + } + } + + return nil +} + +func runSingleBenchmark(config *benchmarkConfig) error { + fmt.Printf("Running benchmarking for %s client:\n", config.clientName) + fmt.Printf( + "\n =====> %s <===== clientCount: %d, concurrentTasks: %d, dataSize: %d \n\n", + config.clientName, + config.clientCount, + config.numConcurrentTasks, + config.dataSize, + ) + + clients, err := createClients(config) + if err != nil { + return err + } + + benchmarkResult := measureBenchmark(clients, config) + if config.resultsFile != os.Stdout { + addJsonResults(config, benchmarkResult) + } + + printResults(benchmarkResult) + fmt.Println() + return closeClients(clients) +} + +type benchmarkClient interface { + connect(connectionSettings *connectionSettings) error + set(key string, value string) (string, error) + get(key string) (string, error) + close() error + getName() string +} + +func createClients(config *benchmarkConfig) ([]benchmarkClient, error) { + var clients []benchmarkClient + for clientNum := 0; clientNum < config.clientCount; clientNum++ { + var client benchmarkClient + switch config.clientName { + case goRedis: + client = &goRedisBenchmarkClient{} + case glide: + client = &glideBenchmarkClient{} + } + + err := client.connect(config.connectionSettings) + if err != nil { + return nil, err + } + + clients = append(clients, client) + } + + return clients, nil +} + +func closeClients(clients []benchmarkClient) error { + for _, client := range clients { + err := client.close() + if err != nil { + return err + } + } + + return nil +} + +var jsonResults []map[string]interface{} + +func writeResults(file *os.File) error { + fileInfo, err := file.Stat() + if err != nil { + return err + } + + if fileInfo.Size() != 0 { + decoder := json.NewDecoder(file) + var existingData []map[string]interface{} + err = decoder.Decode(&existingData) + if err != nil { + return err + } + + jsonResults = append(existingData, jsonResults...) + } + + marshalledJson, err := json.Marshal(jsonResults) + if err != nil { + return err + } + _, err = file.WriteAt(marshalledJson, 0) + if err != nil { + return err + } + + return nil +} + +type benchmarkResults struct { + iterationsPerTask int + duration time.Duration + tps float64 + latencyStats map[string]*latencyStats +} + +func measureBenchmark(clients []benchmarkClient, config *benchmarkConfig) *benchmarkResults { + var iterationsPerTask int + if config.minimal { + iterationsPerTask = 1000 + } else { + iterationsPerTask = int(math.Min(math.Max(1e5, float64(config.numConcurrentTasks*1e4)), 1e7)) + } + + actions := getActions(config.dataSize) + duration, latencies := runBenchmark(iterationsPerTask, config.numConcurrentTasks, actions, clients) + tps := calculateTPS(latencies, duration) + stats := getLatencyStats(latencies) + return &benchmarkResults{ + iterationsPerTask: iterationsPerTask, + duration: duration, + tps: tps, + latencyStats: stats, + } +} + +func calculateTPS(latencies map[string][]time.Duration, totalDuration time.Duration) float64 { + numRequests := 0 + for _, durations := range latencies { + numRequests += len(durations) + } + + return float64(numRequests) / totalDuration.Seconds() +} + +type operations func(client benchmarkClient) (string, error) + +const ( + getExisting = "get_existing" + getNonExisting = "get_non_existing" + set = "set" +) + +func getActions(dataSize int) map[string]operations { + actions := map[string]operations{ + getExisting: func(client benchmarkClient) (string, error) { + return client.get(keyFromExistingKeyspace()) + }, + getNonExisting: func(client benchmarkClient) (string, error) { + return client.get(keyFromNewKeyspace()) + }, + set: func(client benchmarkClient) (string, error) { + return client.set(keyFromExistingKeyspace(), strings.Repeat("0", dataSize)) + }, + } + + return actions +} + +const ( + sizeNewKeyspace = 3750000 + sizeExistingKeyspace = 3000000 +) + +func keyFromExistingKeyspace() string { + randNum, err := rand.Int(rand.Reader, big.NewInt(sizeExistingKeyspace)) + if err != nil { + log.Fatal("Error while generating random number for existing keyspace: ", err) + } + + return fmt.Sprint(randNum.Int64() + 1) +} + +func keyFromNewKeyspace() string { + var totalRange int64 = sizeNewKeyspace - sizeExistingKeyspace + randNum, err := rand.Int(rand.Reader, big.NewInt(totalRange)) + if err != nil { + log.Fatal("Error while generating random number for existing keyspace: ", err) + } + + return fmt.Sprint(randNum.Int64() + sizeExistingKeyspace + 1) +} + +type actionLatency struct { + action string + latency time.Duration +} + +func runBenchmark( + iterationsPerTask int, + concurrentTasks int, + actions map[string]operations, + clients []benchmarkClient, +) (totalDuration time.Duration, latencies map[string][]time.Duration) { + latencies = map[string][]time.Duration{ + getExisting: {}, + getNonExisting: {}, + set: {}, + } + + numResults := concurrentTasks * iterationsPerTask + results := make(chan *actionLatency, numResults) + start := time.Now() + for i := 0; i < concurrentTasks; i++ { + go runTask(results, iterationsPerTask, actions, clients) + } + + for i := 0; i < numResults; i++ { + result := <-results + latencies[result.action] = append(latencies[result.action], result.latency) + } + + return time.Since(start), latencies +} + +func runTask(results chan<- *actionLatency, iterations int, actions map[string]operations, clients []benchmarkClient) { + for i := 0; i < iterations; i++ { + clientIndex := i % len(clients) + action := randomAction() + operation := actions[action] + latency := measureOperation(operation, clients[clientIndex]) + results <- &actionLatency{action: action, latency: latency} + } +} + +func measureOperation(operation operations, client benchmarkClient) time.Duration { + start := time.Now() + _, err := operation(client) + duration := time.Since(start) + if err != nil { + log.Print("error while executing operation: ", err) + } + + return duration +} + +const ( + probGet = 0.8 + probGetExistingKey = 0.8 +) + +// randFloat generates a random float64 in the range [0.0, 1.0) +func randFloat() float64 { + // 1 << 53 is used because a float64 value contains 53 bits for the mantissa and 11 other bits. + randInt, err := rand.Int(rand.Reader, big.NewInt(1<<53)) + if err != nil { + log.Fatal("Error creating random float64: ", err) + } + return float64(randInt.Int64()) / (1 << 53) +} + +func randomAction() string { + if randFloat() > probGet { + return set + } + + if randFloat() > probGetExistingKey { + return getNonExisting + } + + return getExisting +} + +type latencyStats struct { + avgLatency time.Duration + p50Latency time.Duration + p90Latency time.Duration + p99Latency time.Duration + stdDeviation time.Duration + numRequests int +} + +func getLatencyStats(actionLatencies map[string][]time.Duration) map[string]*latencyStats { + results := make(map[string]*latencyStats) + + for action, latencies := range actionLatencies { + sort.Slice(latencies, func(i, j int) bool { + return latencies[i] < latencies[j] + }) + + results[action] = &latencyStats{ + // TODO: Replace with a stats library, eg https://pkg.go.dev/github.com/montanaflynn/stats + avgLatency: average(latencies), + p50Latency: percentile(latencies, 50), + p90Latency: percentile(latencies, 90), + p99Latency: percentile(latencies, 99), + stdDeviation: standardDeviation(latencies), + numRequests: len(latencies), + } + } + + return results +} + +func average(observations []time.Duration) time.Duration { + var sumNano int64 = 0 + for _, observation := range observations { + sumNano += observation.Nanoseconds() + } + + avgNano := sumNano / int64(len(observations)) + return time.Duration(avgNano) +} + +func percentile(observations []time.Duration, p float64) time.Duration { + N := float64(len(observations)) + n := (N-1)*p/100 + 1 + + if n == 1.0 { + return observations[0] + } else if n == N { + return observations[int(N)-1] + } + + k := int(n) + d := n - float64(k) + interpolatedValue := float64(observations[k-1]) + d*(float64(observations[k])-float64(observations[k-1])) + return time.Duration(int64(math.Round(interpolatedValue))) +} + +func standardDeviation(observations []time.Duration) time.Duration { + var sum, mean, sd float64 + numObservations := len(observations) + for i := 0; i < numObservations; i++ { + sum += float64(observations[i]) + } + + mean = sum / float64(numObservations) + for j := 0; j < numObservations; j++ { + sd += math.Pow(float64(observations[j])-mean, 2) + } + + sd = math.Sqrt(sd / float64(numObservations)) + return time.Duration(sd) +} + +func printResults(results *benchmarkResults) { + fmt.Printf("Runtime (sec): %.3f\n", results.duration.Seconds()) + fmt.Printf("Iterations: %d\n", results.iterationsPerTask) + fmt.Printf("TPS: %d\n", int(results.tps)) + + var totalRequests int + for action, latencyStat := range results.latencyStats { + fmt.Printf("===> %s <===\n", action) + fmt.Printf("avg. latency (ms): %.3f\n", latencyStat.avgLatency.Seconds()*1000) + fmt.Printf("std dev (ms): %.3f\n", latencyStat.stdDeviation.Seconds()*1000) + fmt.Printf("p50 latency (ms): %.3f\n", latencyStat.p50Latency.Seconds()*1000) + fmt.Printf("p90 latency (ms): %.3f\n", latencyStat.p90Latency.Seconds()*1000) + fmt.Printf("p99 latency (ms): %.3f\n", latencyStat.p99Latency.Seconds()*1000) + fmt.Printf("Number of requests: %d\n", latencyStat.numRequests) + totalRequests += latencyStat.numRequests + } + + fmt.Printf("Total requests: %d\n", totalRequests) +} + +func addJsonResults(config *benchmarkConfig, results *benchmarkResults) { + jsonResult := make(map[string]interface{}) + + jsonResult["client"] = config.clientName + jsonResult["is_cluster"] = config.connectionSettings.clusterModeEnabled + jsonResult["num_of_tasks"] = config.numConcurrentTasks + jsonResult["data_size"] = config.dataSize + jsonResult["client_count"] = config.clientCount + jsonResult["tps"] = results.tps + + for key, value := range results.latencyStats { + jsonResult[key+"_p50_latency"] = value.p50Latency.Seconds() * 1000 + jsonResult[key+"_p90_latency"] = value.p90Latency.Seconds() * 1000 + jsonResult[key+"_p99_latency"] = value.p99Latency.Seconds() * 1000 + jsonResult[key+"_average_latency"] = value.avgLatency.Seconds() * 1000 + jsonResult[key+"_std_dev"] = value.stdDeviation.Seconds() * 1000 + } + + jsonResults = append(jsonResults, jsonResult) +} diff --git a/go/benchmarks/glide_benchmark_client.go b/go/benchmarks/glide_benchmark_client.go new file mode 100644 index 0000000000..6ae9f4d8d4 --- /dev/null +++ b/go/benchmarks/glide_benchmark_client.go @@ -0,0 +1,54 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package main + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api" +) + +type glideBenchmarkClient struct { + client api.BaseClient +} + +func (glideBenchmarkClient *glideBenchmarkClient) connect(connectionSettings *connectionSettings) error { + if connectionSettings.clusterModeEnabled { + config := api.NewGlideClusterClientConfiguration(). + WithAddress(&api.NodeAddress{Host: connectionSettings.host, Port: connectionSettings.port}). + WithUseTLS(connectionSettings.useTLS) + glideClient, err := api.NewGlideClusterClient(config) + if err != nil { + return err + } + + glideBenchmarkClient.client = glideClient + return nil + } else { + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Host: connectionSettings.host, Port: connectionSettings.port}). + WithUseTLS(connectionSettings.useTLS) + glideClient, err := api.NewGlideClient(config) + if err != nil { + return err + } + + glideBenchmarkClient.client = glideClient + return nil + } +} + +func (glideBenchmarkClient *glideBenchmarkClient) get(key string) (string, error) { + return glideBenchmarkClient.client.Get(key) +} + +func (glideBenchmarkClient *glideBenchmarkClient) set(key string, value string) (string, error) { + return glideBenchmarkClient.client.Set(key, value) +} + +func (glideBenchmarkClient *glideBenchmarkClient) close() error { + glideBenchmarkClient.client.Close() + return nil +} + +func (glideBenchmarkClient *glideBenchmarkClient) getName() string { + return "glide" +} diff --git a/go/benchmarks/go.mod b/go/benchmarks/go.mod new file mode 100644 index 0000000000..93dc587fb3 --- /dev/null +++ b/go/benchmarks/go.mod @@ -0,0 +1,16 @@ +module github.com/valkey-io/valkey-glide/go/glide/benchmarks + +go 1.18 + +replace github.com/valkey-io/valkey-glide/go/glide => ../ + +require ( + github.com/valkey-io/valkey-glide/go/glide v0.0.0 + github.com/redis/go-redis/v9 v9.5.1 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go/benchmarks/go.sum b/go/benchmarks/go.sum new file mode 100644 index 0000000000..3d1b358231 --- /dev/null +++ b/go/benchmarks/go.sum @@ -0,0 +1,15 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/benchmarks/go_redis_benchmark_client.go b/go/benchmarks/go_redis_benchmark_client.go new file mode 100644 index 0000000000..1909753fce --- /dev/null +++ b/go/benchmarks/go_redis_benchmark_client.go @@ -0,0 +1,71 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package main + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + + "github.com/redis/go-redis/v9" +) + +type goRedisBenchmarkClient struct { + client redis.Cmdable +} + +func (goRedisClient *goRedisBenchmarkClient) connect(connectionSettings *connectionSettings) error { + if connectionSettings.clusterModeEnabled { + clusterOptions := &redis.ClusterOptions{ + Addrs: []string{fmt.Sprintf("%s:%d", connectionSettings.host, connectionSettings.port)}, + } + + if connectionSettings.useTLS { + clusterOptions.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12} + } + + goRedisClient.client = redis.NewClusterClient(clusterOptions) + } else { + options := &redis.Options{ + Addr: fmt.Sprintf("%s:%d", connectionSettings.host, connectionSettings.port), + DB: 0, + } + + if connectionSettings.useTLS { + options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12} + } + + goRedisClient.client = redis.NewClient(options) + } + + return goRedisClient.client.Ping(context.Background()).Err() +} + +func (goRedisClient *goRedisBenchmarkClient) set(key string, value string) (string, error) { + return goRedisClient.client.Set(context.Background(), key, value, 0).Result() +} + +func (goRedisClient *goRedisBenchmarkClient) get(key string) (string, error) { + value, err := goRedisClient.client.Get(context.Background(), key).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return "", err + } + + return value, nil +} + +func (goRedisClient *goRedisBenchmarkClient) close() error { + switch c := goRedisClient.client.(type) { + case *redis.Client: + return c.Close() + case *redis.ClusterClient: + return c.Close() + default: + return fmt.Errorf("unsupported client type") + } +} + +func (goRedisClient *goRedisBenchmarkClient) getName() string { + return "go-redis" +} diff --git a/go/benchmarks/main.go b/go/benchmarks/main.go new file mode 100644 index 0000000000..4614334290 --- /dev/null +++ b/go/benchmarks/main.go @@ -0,0 +1,195 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +type options struct { + resultsFile string + dataSize string + concurrentTasks string + clients string + host string + port int + clientCount string + tls bool + clusterModeEnabled bool + minimal bool +} + +type runConfiguration struct { + resultsFile *os.File + dataSize []int + concurrentTasks []int + clientNames []string + host string + port int + clientCount []int + tls bool + clusterModeEnabled bool + minimal bool +} + +const ( + goRedis = "go-redis" + glide = "glide" + all = "all" +) + +func main() { + opts := parseArguments() + + runConfig, err := verifyOptions(opts) + if err != nil { + log.Fatal("Error verifying options:", err) + return + } + + if runConfig.resultsFile != os.Stdout { + defer closeFile(runConfig.resultsFile) + } + + err = runBenchmarks(runConfig) + if err != nil { + log.Fatal("Error running benchmarking:", err) + } +} + +func closeFile(file *os.File) { + err := file.Close() + if err != nil { + log.Fatal("Error closing file:", err) + } +} + +func parseArguments() *options { + resultsFile := flag.String("resultsFile", "results/go-results.json", "Result filepath") + dataSize := flag.String("dataSize", "[100]", "Data block size") + concurrentTasks := flag.String("concurrentTasks", "[1 10 100 1000]", "Number of concurrent tasks") + clientNames := flag.String("clients", "all", "One of: all|go-redis|glide") + host := flag.String("host", "localhost", "Hostname") + port := flag.Int("port", 6379, "Port number") + clientCount := flag.String("clientCount", "[1]", "Number of clients to run") + tls := flag.Bool("tls", false, "Use TLS") + clusterModeEnabled := flag.Bool("clusterModeEnabled", false, "Is cluster mode enabled") + minimal := flag.Bool("minimal", false, "Run benchmark in minimal mode") + + flag.Parse() + + return &options{ + resultsFile: *resultsFile, + dataSize: *dataSize, + concurrentTasks: *concurrentTasks, + clients: *clientNames, + host: *host, + port: *port, + clientCount: *clientCount, + tls: *tls, + clusterModeEnabled: *clusterModeEnabled, + minimal: *minimal, + } +} + +func verifyOptions(opts *options) (*runConfiguration, error) { + var runConfig runConfiguration + var err error + + if opts.resultsFile == "" { + runConfig.resultsFile = os.Stdout + } else if _, err = os.Stat(opts.resultsFile); err == nil { + // File exists + runConfig.resultsFile, err = os.OpenFile(opts.resultsFile, os.O_RDWR, os.ModePerm) + if err != nil { + return nil, err + } + } else if errors.Is(err, os.ErrNotExist) { + // File does not exist + err = os.MkdirAll(filepath.Dir(opts.resultsFile), os.ModePerm) + if err != nil { + return nil, err + } + + runConfig.resultsFile, err = os.Create(opts.resultsFile) + if err != nil { + return nil, err + } + } else { + // Some other error occurred + return nil, err + } + + runConfig.concurrentTasks, err = parseOptionsIntList(opts.concurrentTasks) + if err != nil { + return nil, fmt.Errorf("invalid concurrentTasks option: %v", err) + } + + runConfig.dataSize, err = parseOptionsIntList(opts.dataSize) + if err != nil { + return nil, fmt.Errorf("invalid dataSize option: %v", err) + } + + runConfig.clientCount, err = parseOptionsIntList(opts.clientCount) + if err != nil { + return nil, fmt.Errorf("invalid clientCount option: %v", err) + } + + switch { + case strings.EqualFold(opts.clients, goRedis): + runConfig.clientNames = append(runConfig.clientNames, goRedis) + + case strings.EqualFold(opts.clients, glide): + runConfig.clientNames = append(runConfig.clientNames, glide) + + case strings.EqualFold(opts.clients, all): + runConfig.clientNames = append(runConfig.clientNames, goRedis, glide) + default: + return nil, fmt.Errorf("invalid clients option, should be one of: all|go-redis|glide") + } + + runConfig.host = opts.host + runConfig.port = opts.port + runConfig.tls = opts.tls + runConfig.clusterModeEnabled = opts.clusterModeEnabled + runConfig.minimal = opts.minimal + + return &runConfig, nil +} + +func parseOptionsIntList(listAsString string) ([]int, error) { + listAsString = strings.Trim(strings.TrimSpace(listAsString), "[]") + if len(listAsString) == 0 { + return nil, fmt.Errorf("option is empty or contains only brackets") + } + + matched, err := regexp.MatchString("^\\d+(\\s+\\d+)*$", listAsString) + if err != nil { + return nil, err + } + + if !matched { + return nil, fmt.Errorf("wrong format for option") + } + + stringList := strings.Split(listAsString, " ") + var intList []int + for _, intString := range stringList { + num, err := strconv.Atoi(strings.TrimSpace(intString)) + if err != nil { + return nil, fmt.Errorf("wrong number format for option: %s", intString) + } + + intList = append(intList, num) + } + + return intList, nil +} diff --git a/go/examples/main.go b/go/examples/main.go new file mode 100644 index 0000000000..d2b9761335 --- /dev/null +++ b/go/examples/main.go @@ -0,0 +1,56 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "log" + + "github.com/valkey-io/valkey-glide/go/glide/api" +) + +// TODO: Update the file based on the template used in other clients. +func main() { + host := "localhost" + port := 6379 + + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Host: host, Port: port}) + + client, err := api.NewGlideClient(config) + if err != nil { + log.Fatal("error connecting to database: ", err) + } + + res, err := client.CustomCommand([]string{"PING"}) + if err != nil { + log.Fatal("Glide example failed with an error: ", err) + } + fmt.Println("PING:", res) + + res, err = client.Set("apples", "oran\x00ges") + if err != nil { + log.Fatal("Glide example failed with an error: ", err) + } + fmt.Println("SET(apples, oranges):", res) + + res, err = client.Get("invalidKey") + if err != nil { + log.Fatal("Glide example failed with an error: ", err) + } + fmt.Println("GET(invalidKey):", res) + + res, err = client.Get("apples") + if err != nil { + log.Fatal("Glide example failed with an error: ", err) + } + fmt.Println("GET(apples):", res) + + res, err = client.Get("app\x00les") + if err != nil { + log.Fatal("Glide example failed with an error: ", err) + } + fmt.Println("GET(app\x00les):", res) + + client.Close() +} diff --git a/go/go.mod b/go/go.mod index 21aaf9f4a7..8ce0f5f6fd 100644 --- a/go/go.mod +++ b/go/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect diff --git a/go/go.sum b/go/go.sum index cdbe2b7522..bb96255d9f 100644 --- a/go/go.sum +++ b/go/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/go/integTest/connection_test.go b/go/integTest/connection_test.go new file mode 100644 index 0000000000..910772769b --- /dev/null +++ b/go/integTest/connection_test.go @@ -0,0 +1,55 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" +) + +func (suite *GlideTestSuite) TestStandaloneConnect() { + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}) + client, err := api.NewGlideClient(config) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), client) + + client.Close() +} + +func (suite *GlideTestSuite) TestClusterConnect() { + config := api.NewGlideClusterClientConfiguration() + for _, port := range suite.clusterPorts { + config.WithAddress(&api.NodeAddress{Port: port}) + } + + client, err := api.NewGlideClusterClient(config) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), client) + + client.Close() +} + +func (suite *GlideTestSuite) TestClusterConnect_singlePort() { + config := api.NewGlideClusterClientConfiguration(). + WithAddress(&api.NodeAddress{Port: suite.clusterPorts[0]}) + + client, err := api.NewGlideClusterClient(config) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), client) + + client.Close() +} + +func (suite *GlideTestSuite) TestConnectWithInvalidAddress() { + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Host: "invalid-host"}) + client, err := api.NewGlideClient(config) + + assert.Nil(suite.T(), client) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.ConnectionError{}, err) +} diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go new file mode 100644 index 0000000000..7abf5df27c --- /dev/null +++ b/go/integTest/glide_test_suite_test.go @@ -0,0 +1,192 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/valkey-io/valkey-glide/go/glide/api" +) + +type GlideTestSuite struct { + suite.Suite + standalonePorts []int + clusterPorts []int + redisVersion string + clients []*api.GlideClient + clusterClients []*api.GlideClusterClient +} + +func (suite *GlideTestSuite) SetupSuite() { + // Stop cluster in case previous test run was interrupted or crashed and didn't stop. + // If an error occurs, we ignore it in case the servers actually were stopped before running this. + runClusterManager(suite, []string{"stop", "--prefix", "redis-cluster"}, true) + + // Delete dirs if stop failed due to https://github.com/valkey-io/valkey-glide/issues/849 + err := os.RemoveAll("../../utils/clusters") + if err != nil && !os.IsNotExist(err) { + log.Fatal(err) + } + + // Start standalone instance + clusterManagerOutput := runClusterManager(suite, []string{"start", "-r", "0"}, false) + + suite.standalonePorts = extractPorts(suite, clusterManagerOutput) + suite.T().Logf("Standalone ports = %s", fmt.Sprint(suite.standalonePorts)) + + // Start cluster + clusterManagerOutput = runClusterManager(suite, []string{"start", "--cluster-mode"}, false) + + suite.clusterPorts = extractPorts(suite, clusterManagerOutput) + suite.T().Logf("Cluster ports = %s", fmt.Sprint(suite.clusterPorts)) + + // Get Redis version + byteOutput, err := exec.Command("redis-server", "-v").Output() + if err != nil { + suite.T().Fatal(err.Error()) + } + + suite.redisVersion = extractServerVersion(string(byteOutput)) + suite.T().Logf("Redis version = %s", suite.redisVersion) +} + +func extractPorts(suite *GlideTestSuite, output string) []int { + var ports []int + for _, line := range strings.Split(output, "\n") { + if !strings.HasPrefix(line, "CLUSTER_NODES=") { + continue + } + + addresses := strings.Split(line, "=")[1] + addressList := strings.Split(addresses, ",") + for _, address := range addressList { + portString := strings.Split(address, ":")[1] + port, err := strconv.Atoi(portString) + if err != nil { + suite.T().Fatalf("Failed to parse port from cluster_manager.py output: %s", err.Error()) + } + + ports = append(ports, port) + } + } + + return ports +} + +func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool) string { + pythonArgs := append([]string{"../../utils/cluster_manager.py"}, args...) + output, err := exec.Command("python3", pythonArgs...).Output() + if len(output) > 0 { + suite.T().Logf("cluster_manager.py stdout:\n====\n%s\n====\n", string(output)) + } + + if err != nil { + var exitError *exec.ExitError + isExitError := errors.As(err, &exitError) + if !isExitError { + suite.T().Fatalf("Unexpected error while executing cluster_manager.py: %s", err.Error()) + } + + if exitError.Stderr != nil && len(exitError.Stderr) > 0 { + suite.T().Logf("cluster_manager.py stderr:\n====\n%s\n====\n", string(exitError.Stderr)) + } + + if !ignoreExitCode { + suite.T().Fatalf("cluster_manager.py script failed: %s", exitError.Error()) + } + } + + return string(output) +} + +func extractServerVersion(output string) string { + // Redis response: + // Redis server v=7.2.3 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=7504b1fedf883f2 + // Valkey response: + // Server v=7.2.5 sha=26388270:0 malloc=jemalloc-5.3.0 bits=64 build=ea40bb1576e402d6 + versionSection := strings.Split(output, "v=")[1] + return strings.Split(versionSection, " ")[0] +} + +func TestGlideTestSuite(t *testing.T) { + suite.Run(t, new(GlideTestSuite)) +} + +func (suite *GlideTestSuite) TearDownSuite() { + runClusterManager(suite, []string{"stop", "--prefix", "redis-cluster", "--keep-folder"}, false) +} + +func (suite *GlideTestSuite) TearDownTest() { + for _, client := range suite.clients { + client.Close() + } + + for _, client := range suite.clusterClients { + client.Close() + } +} + +func (suite *GlideTestSuite) runWithDefaultClients(test func(client api.BaseClient)) { + clients := suite.getDefaultClients() + suite.runWithClients(clients, test) +} + +func (suite *GlideTestSuite) getDefaultClients() []api.BaseClient { + return []api.BaseClient{suite.defaultClient(), suite.defaultClusterClient()} +} + +func (suite *GlideTestSuite) defaultClient() *api.GlideClient { + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}). + WithRequestTimeout(5000) + return suite.client(config) +} + +func (suite *GlideTestSuite) client(config *api.GlideClientConfiguration) *api.GlideClient { + client, err := api.NewGlideClient(config) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), client) + + suite.clients = append(suite.clients, client) + return client +} + +func (suite *GlideTestSuite) defaultClusterClient() *api.GlideClusterClient { + config := api.NewGlideClusterClientConfiguration(). + WithAddress(&api.NodeAddress{Port: suite.clusterPorts[0]}). + WithRequestTimeout(5000) + return suite.clusterClient(config) +} + +func (suite *GlideTestSuite) clusterClient(config *api.GlideClusterClientConfiguration) *api.GlideClusterClient { + client, err := api.NewGlideClusterClient(config) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), client) + + suite.clusterClients = append(suite.clusterClients, client) + return client +} + +func (suite *GlideTestSuite) runWithClients(clients []api.BaseClient, test func(client api.BaseClient)) { + for i, client := range clients { + suite.T().Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { + test(client) + }) + } +} + +func (suite *GlideTestSuite) verifyOK(result string, err error) { + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.OK, result) +} diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go new file mode 100644 index 0000000000..793a6fb024 --- /dev/null +++ b/go/integTest/shared_commands_test.go @@ -0,0 +1,172 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "time" + + "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" +) + +const ( + keyName = "key" + initialValue = "value" + anotherValue = "value2" +) + +func (suite *GlideTestSuite) TestSetAndGet_noOptions() { + suite.runWithDefaultClients(func(client api.BaseClient) { + suite.verifyOK(client.Set(keyName, initialValue)) + result, err := client.Get(keyName) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result) + }) +} + +func (suite *GlideTestSuite) TestSetAndGet_byteString() { + suite.runWithDefaultClients(func(client api.BaseClient) { + invalidUTF8Value := "\xff\xfe\xfd" + suite.verifyOK(client.Set(keyName, invalidUTF8Value)) + result, err := client.Get(keyName) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), invalidUTF8Value, result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue() { + suite.runWithDefaultClients(func(client api.BaseClient) { + suite.verifyOK(client.Set(keyName, initialValue)) + + opts := &api.SetOptions{ReturnOldValue: true} + result, err := client.SetWithOptions(keyName, anotherValue, opts) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_overwrite() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_OnlyIfExists_overwrite" + suite.verifyOK(client.Set(key, initialValue)) + + opts := &api.SetOptions{ConditionalSet: api.OnlyIfExists} + suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) + + result, err := client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), anotherValue, result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_missingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_OnlyIfExists_missingKey" + opts := &api.SetOptions{ConditionalSet: api.OnlyIfExists} + result, err := client.SetWithOptions(key, anotherValue, opts) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_missingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_OnlyIfDoesNotExist_missingKey" + opts := &api.SetOptions{ConditionalSet: api.OnlyIfDoesNotExist} + suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) + + result, err := client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), anotherValue, result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_existingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_OnlyIfDoesNotExist_existingKey" + opts := &api.SetOptions{ConditionalSet: api.OnlyIfDoesNotExist} + suite.verifyOK(client.Set(key, initialValue)) + + result, err := client.SetWithOptions(key, anotherValue, opts) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result) + + result, err = client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_KeepExistingExpiry" + opts := &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(2000)}} + suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) + + result, err := client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result) + + opts = &api.SetOptions{Expiry: &api.Expiry{Type: api.KeepExisting}} + suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) + + result, err = client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), anotherValue, result) + + time.Sleep(2222 * time.Millisecond) + result, err = client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_UpdateExistingExpiry" + opts := &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(100500)}} + suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) + + result, err := client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result) + + opts = &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(2000)}} + suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) + + result, err = client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), anotherValue, result) + + time.Sleep(2222 * time.Millisecond) + result, err = client.Get(key) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result) + }) +} + +func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue_nonExistentKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestSetWithOptions_ReturnOldValue_nonExistentKey" + opts := &api.SetOptions{ReturnOldValue: true} + + result, err := client.SetWithOptions(key, anotherValue, opts) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go new file mode 100644 index 0000000000..816cdcf89e --- /dev/null +++ b/go/integTest/standalone_commands_test.go @@ -0,0 +1,74 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package integTest + +import ( + "fmt" + "strings" + + "github.com/valkey-io/valkey-glide/go/glide/api" + + "github.com/stretchr/testify/assert" +) + +func (suite *GlideTestSuite) TestCustomCommandInfo() { + client := suite.defaultClient() + result, err := client.CustomCommand([]string{"INFO"}) + + assert.Nil(suite.T(), err) + assert.IsType(suite.T(), "", result) + strResult := result.(string) + assert.True(suite.T(), strings.Contains(strResult, "# Stats")) +} + +func (suite *GlideTestSuite) TestCustomCommandPing() { + client := suite.defaultClient() + result, err := client.CustomCommand([]string{"PING"}) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "PONG", result) +} + +func (suite *GlideTestSuite) TestCustomCommandClientInfo() { + clientName := "TEST_CLIENT_NAME" + config := api.NewGlideClientConfiguration(). + WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}). + WithClientName(clientName) + client := suite.client(config) + + result, err := client.CustomCommand([]string{"CLIENT", "INFO"}) + + assert.Nil(suite.T(), err) + assert.IsType(suite.T(), "", result) + strResult := result.(string) + assert.True(suite.T(), strings.Contains(strResult, fmt.Sprintf("name=%s", clientName))) +} + +func (suite *GlideTestSuite) TestCustomCommand_invalidCommand() { + client := suite.defaultClient() + result, err := client.CustomCommand([]string{"pewpew"}) + + assert.Nil(suite.T(), result) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) +} + +func (suite *GlideTestSuite) TestCustomCommand_invalidArgs() { + client := suite.defaultClient() + result, err := client.CustomCommand([]string{"ping", "pang", "pong"}) + + assert.Nil(suite.T(), result) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) +} + +func (suite *GlideTestSuite) TestCustomCommand_closedClient() { + client := suite.defaultClient() + client.Close() + + result, err := client.CustomCommand([]string{"ping"}) + + assert.Nil(suite.T(), result) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.ClosingError{}, err) +} diff --git a/go/src/lib.rs b/go/src/lib.rs index bd76ebe347..a1cf07dda8 100644 --- a/go/src/lib.rs +++ b/go/src/lib.rs @@ -1,40 +1,73 @@ /* - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -// TODO: Investigate using uniffi bindings for Go instead of cbindgen +* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +*/ #![deny(unsafe_op_in_unsafe_fn)] - +use derivative::Derivative; use glide_core::client::Client as GlideClient; use glide_core::connection_request; use glide_core::errors; use glide_core::errors::RequestErrorType; +use glide_core::request_type::RequestType; use glide_core::ConnectionRequest; use protobuf::Message; +use redis::{RedisResult, Value}; +use std::slice::from_raw_parts; use std::{ ffi::{c_void, CString}, - os::raw::c_char, + mem, + os::raw::{c_char, c_double, c_long, c_ulong}, }; use tokio::runtime::Builder; use tokio::runtime::Runtime; -/// Success callback that is called when a Redis command succeeds. +/// The struct represents the response of the command. +/// +/// It will have one of the value populated depending on the return type of the command. +/// +/// The struct is freed by the external caller by using `free_command_response` to avoid memory leaks. +/// TODO: Free the array pointer. +#[repr(C)] +#[derive(Derivative)] +#[derivative(Debug, Default)] +pub struct CommandResponse { + int_value: c_long, + float_value: c_double, + bool_value: bool, + + // Below two values are related to each other. + // `string_value` represents the string. + // `string_value_len` represents the length of the string. + #[derivative(Default(value = "std::ptr::null_mut()"))] + string_value: *mut c_char, + string_value_len: c_long, + + // Below three values are related to each other. + // `array_value` represents the array of strings. + // `array_elements_len` represents the length of each array element. + // `array_value_len` represents the length of the array. + #[derivative(Default(value = "std::ptr::null_mut()"))] + array_value: *mut *mut c_char, + #[derivative(Default(value = "std::ptr::null_mut()"))] + array_elements_len: *mut c_long, + array_value_len: c_long, +} + +/// Success callback that is called when a command succeeds. /// /// The success callback needs to copy the given string synchronously, since it will be dropped by Rust once the callback returns. The callback should be offloaded to a separate thread in order not to exhaust the client's thread pool. /// /// `index_ptr` is a baton-pass back to the caller language to uniquely identify the promise. -/// `message` is the value returned by the Redis command. The 'message' is managed by Rust and is freed when the callback returns control back to the caller. -// TODO: Change message type when implementing command logic -// TODO: Consider using a single response callback instead of success and failure callbacks -pub type SuccessCallback = unsafe extern "C" fn(index_ptr: usize, message: *const c_char) -> (); +/// `message` is the value returned by the command. The 'message' is managed by Rust and is freed when the callback returns control back to the caller. +pub type SuccessCallback = + unsafe extern "C" fn(index_ptr: usize, message: *const CommandResponse) -> (); -/// Failure callback that is called when a Redis command fails. +/// Failure callback that is called when a command fails. /// /// The failure callback needs to copy the given string synchronously, since it will be dropped by Rust once the callback returns. The callback should be offloaded to a separate thread in order not to exhaust the client's thread pool. /// /// `index_ptr` is a baton-pass back to the caller language to uniquely identify the promise. -/// `error_message` is the error message returned by Redis for the failed command. The 'error_message' is managed by Rust and is freed when the callback returns control back to the caller. +/// `error_message` is the error message returned by server for the failed command. The 'error_message' is managed by Rust and is freed when the callback returns control back to the caller. /// `error_type` is the type of error returned by glide-core, depending on the `RedisError` returned. pub type FailureCallback = unsafe extern "C" fn( index_ptr: usize, @@ -74,7 +107,7 @@ fn create_client_internal( let runtime = Builder::new_multi_thread() .enable_all() .worker_threads(1) - .thread_name("GLIDE for Redis Go thread") + .thread_name("Valkey-GLIDE Go thread") .build() .map_err(|err| { let redis_error = err.into(); @@ -97,8 +130,8 @@ fn create_client_internal( /// /// `connection_request_bytes` is an array of bytes that will be parsed into a Protobuf `ConnectionRequest` object. /// `connection_request_len` is the number of bytes in `connection_request_bytes`. -/// `success_callback` is the callback that will be called when a Redis command succeeds. -/// `failure_callback` is the callback that will be called when a Redis command fails. +/// `success_callback` is the callback that will be called when a command succeeds. +/// `failure_callback` is the callback that will be called when a command fails. /// /// # Safety /// @@ -120,7 +153,9 @@ pub unsafe extern "C" fn create_client( let response = match create_client_internal(request_bytes, success_callback, failure_callback) { Err(err) => ConnectionResponse { conn_ptr: std::ptr::null(), - connection_error_message: CString::into_raw(CString::new(err).unwrap()), + connection_error_message: CString::into_raw( + CString::new(err).expect("Couldn't convert error message to CString"), + ), }, Ok(client) => ConnectionResponse { conn_ptr: Box::into_raw(Box::new(client)) as *const c_void, @@ -147,7 +182,7 @@ pub unsafe extern "C" fn create_client( // TODO: Ensure safety when command has not completed yet #[no_mangle] pub unsafe extern "C" fn close_client(client_adapter_ptr: *const c_void) { - assert!(client_adapter_ptr.is_null()); + assert!(!client_adapter_ptr.is_null()); drop(unsafe { Box::from_raw(client_adapter_ptr as *mut ClientAdapter) }); } @@ -170,7 +205,7 @@ pub unsafe extern "C" fn close_client(client_adapter_ptr: *const c_void) { pub unsafe extern "C" fn free_connection_response( connection_response_ptr: *mut ConnectionResponse, ) { - assert!(connection_response_ptr.is_null()); + assert!(!connection_response_ptr.is_null()); let connection_response = unsafe { Box::from_raw(connection_response_ptr) }; let connection_error_message = connection_response.connection_error_message; drop(connection_response); @@ -178,3 +213,172 @@ pub unsafe extern "C" fn free_connection_response( drop(unsafe { CString::from_raw(connection_error_message as *mut c_char) }); } } + +/// Deallocates a `CommandResponse`. +/// +/// This function also frees the contained string_value and array_value. If the string_value and array_value are null pointers, the function returns and only the `CommandResponse` is freed. +/// +/// # Panics +/// +/// This function panics when called with a null `CommandResponse` pointer. +/// +/// # Safety +/// +/// * `free_command_response` can only be called once per `CommandResponse`. Calling it twice is undefined behavior, since the address will be freed twice. +/// * `command_response_ptr` must be obtained from the `CommandResponse` returned in [`SuccessCallback`] from [`command`]. +/// * `command_response_ptr` must be valid until `free_command_response` is called. +/// * The contained `string_value` must be obtained from the `CommandResponse` returned in [`SuccessCallback`] from [`command`]. +/// * The contained `string_value` must be valid until `free_command_response` is called and it must outlive the `CommandResponse` that contains it. +#[no_mangle] +pub unsafe extern "C" fn free_command_response(command_response_ptr: *mut CommandResponse) { + assert!(!command_response_ptr.is_null()); + let command_response = unsafe { Box::from_raw(command_response_ptr) }; + let string_value = command_response.string_value; + let string_value_len = command_response.string_value_len; + drop(command_response); + if !string_value.is_null() { + let len = string_value_len as usize; + unsafe { Vec::from_raw_parts(string_value, len, len) }; + } +} + +/// Frees the error_message received on a command failure. +/// +/// # Panics +/// +/// This functions panics when called with a null `c_char` pointer. +/// +/// # Safety +/// +/// `free_error_message` can only be called once per `error_message`. Calling it twice is undefined behavior, since the address will be freed twice. +#[no_mangle] +pub unsafe extern "C" fn free_error_message(error_message: *mut c_char) { + assert!(!error_message.is_null()); + drop(unsafe { CString::from_raw(error_message as *mut c_char) }); +} + +/// Converts a double pointer to a vec. +unsafe fn convert_double_pointer_to_vec( + data: *const *const c_char, + len: c_ulong, + data_len: *const c_ulong, +) -> Vec> { + let string_ptrs = unsafe { from_raw_parts(data, len as usize) }; + let string_lengths = unsafe { from_raw_parts(data_len, len as usize) }; + let mut result: Vec> = Vec::new(); + for (i, &str_ptr) in string_ptrs.iter().enumerate() { + let slice = unsafe { from_raw_parts(str_ptr as *const u8, string_lengths[i] as usize) }; + result.push(slice.to_vec()); + } + result +} + +fn convert_vec_to_pointer(mut vec: Vec) -> (*mut T, c_long) { + vec.shrink_to_fit(); + let vec_ptr = vec.as_mut_ptr(); + let len = vec.len() as c_long; + mem::forget(vec); + (vec_ptr, len) +} + +// TODO: Finish documentation +/// Executes a command. +/// +/// # Safety +/// +/// * TODO: finish safety section. +#[no_mangle] +pub unsafe extern "C" fn command( + client_adapter_ptr: *const c_void, + channel: usize, + command_type: RequestType, + arg_count: c_ulong, + args: *const *const c_char, + args_len: *const c_ulong, +) { + let client_adapter = + unsafe { Box::leak(Box::from_raw(client_adapter_ptr as *mut ClientAdapter)) }; + // The safety of this needs to be ensured by the calling code. Cannot dispose of the pointer before all operations have completed. + let ptr_address = client_adapter_ptr as usize; + + let arg_vec = unsafe { convert_double_pointer_to_vec(args, arg_count, args_len) }; + + let mut client_clone = client_adapter.client.clone(); + client_adapter.runtime.spawn(async move { + let mut cmd = command_type + .get_command() + .expect("Couldn't fetch command type"); + for slice in arg_vec { + cmd.arg(slice); + } + + let result = client_clone.send_command(&cmd, None).await; + let client_adapter = unsafe { Box::leak(Box::from_raw(ptr_address as *mut ClientAdapter)) }; + let value = match result { + Ok(value) => value, + Err(err) => { + let message = errors::error_message(&err); + let error_type = errors::error_type(&err); + + let c_err_str = CString::into_raw( + CString::new(message).expect("Couldn't convert error message to CString"), + ); + unsafe { (client_adapter.failure_callback)(channel, c_err_str, error_type) }; + return; + } + }; + + let mut command_response = CommandResponse::default(); + let result: RedisResult> = match value { + Value::Nil => Ok(None), + Value::SimpleString(text) => { + let vec = text.chars().map(|b| b as c_char).collect::>(); + let (vec_ptr, len) = convert_vec_to_pointer(vec); + command_response.string_value = vec_ptr; + command_response.string_value_len = len; + Ok(Some(command_response)) + } + Value::BulkString(text) => { + let vec = text.iter().map(|b| *b as c_char).collect::>(); + let (vec_ptr, len) = convert_vec_to_pointer(vec); + command_response.string_value = vec_ptr; + command_response.string_value_len = len; + Ok(Some(command_response)) + } + Value::VerbatimString { format: _, text } => { + let vec = text.chars().map(|b| b as c_char).collect::>(); + let (vec_ptr, len) = convert_vec_to_pointer(vec); + command_response.string_value = vec_ptr; + command_response.string_value_len = len; + Ok(Some(command_response)) + } + Value::Okay => { + let vec = "OK".chars().map(|b| b as c_char).collect::>(); + let (vec_ptr, len) = convert_vec_to_pointer(vec); + command_response.string_value = vec_ptr; + command_response.string_value_len = len; + Ok(Some(command_response)) + } + // TODO: Add support for other return types. + _ => todo!(), + }; + + unsafe { + match result { + Ok(None) => (client_adapter.success_callback)(channel, std::ptr::null()), + Ok(Some(message)) => { + (client_adapter.success_callback)(channel, Box::into_raw(Box::new(message))) + } + Err(err) => { + let message = errors::error_message(&err); + let error_type = errors::error_type(&err); + + let c_err_str = CString::into_raw( + CString::new(message).expect("Couldn't convert error message to CString"), + ); + (client_adapter.failure_callback)(channel, c_err_str, error_type); + } + }; + } + }); +} From 37b6f1560ea96c99225f79eaba3f7085c45e28c1 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 19 Aug 2024 10:59:52 -0700 Subject: [PATCH 187/236] Node: Add `XREADGROUP` command (#2124) * Add `XINFOGROUP` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + .../api/commands/StreamBaseCommands.java | 36 +-- .../glide/api/models/BaseTransaction.java | 20 +- node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 53 +++- node/src/Commands.ts | 72 +++-- node/src/Transaction.ts | 34 ++- node/tests/GlideClientInternals.test.ts | 1 - node/tests/GlideClusterClient.test.ts | 2 + node/tests/SharedTests.ts | 262 ++++++++++++------ node/tests/TestUtilities.ts | 12 +- python/python/glide/async_commands/core.py | 8 +- .../glide/async_commands/transaction.py | 8 +- 13 files changed, 344 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54312ceb4f..bba3fbafc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) * Java: Added PUBSUB CHANNELS, NUMPAT and NUMSUB commands ([#2105](https://github.com/valkey-io/valkey-glide/pull/2105)) * Java: Added binary support for custom command ([#2109](https://github.com/valkey-io/valkey-glide/pull/2109)) diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index a62000c3c8..57bc86de78 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -199,9 +199,7 @@ CompletableFuture xadd( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @return A {@literal Map>} with stream * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. * @example @@ -225,9 +223,7 @@ CompletableFuture xadd( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @return A {@literal Map>} with stream * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. * @example @@ -252,9 +248,7 @@ CompletableFuture>> xreadBina * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @param options Options detailing how to read the stream {@link StreamReadOptions}. * @return A {@literal Map>} with stream * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. @@ -282,9 +276,7 @@ CompletableFuture>> xread( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @param options Options detailing how to read the stream {@link StreamReadOptions}. * @return A {@literal Map>} with stream * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. @@ -974,9 +966,8 @@ CompletableFuture xgroupSetId( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. Use the special id of {@literal ">"} to receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal ">"} to receive only new messages. * @param group The consumer group name. * @param consumer The consumer name. * @return A {@literal Map>} with stream @@ -1008,9 +999,8 @@ CompletableFuture>> xreadgroup( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. Use the special id of {@literal gs(">")} to receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal gs(">")} to receive only new messages. * @param group The consumer group name. * @param consumer The consumer name. * @return A {@literal Map>} with stream @@ -1042,9 +1032,8 @@ CompletableFuture>> xreadgrou * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. Use the special id of {@literal ">"} to receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal ">"} to receive only new messages. * @param group The consumer group name. * @param consumer The consumer name. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. @@ -1081,9 +1070,8 @@ CompletableFuture>> xreadgroup( * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash * slot. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The - * Map is composed of a stream's key and the id of the entry after which the stream - * will be read. Use the special id of {@literal gs(">")} to receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal gs(">")} to receive only new messages. * @param group The consumer group name. * @param consumer The consumer name. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 1130e30f1c..868e49206f 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -3442,9 +3442,7 @@ public T xadd( * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. * @see valkey.io for details. - * @param keysAndIds An array of Pairs of keys and entry ids to read from. A - * pair is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of * pairings with format [[field, entry], @@ -3460,9 +3458,7 @@ public T xread(@NonNull Map keysAndIds) { * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. * @see valkey.io for details. - * @param keysAndIds An array of Pairs of keys and entry ids to read from. A - * pair is composed of a stream's key and the id of the entry after which the stream - * will be read. + * @param keysAndIds A Map of keys and entry IDs to read from. * @param options options detailing how to read the stream {@link StreamReadOptions}. * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of @@ -3841,10 +3837,8 @@ public T xgroupSetId( * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The Map - * is composed of a stream's key and the id of the entry after which the stream will be read. - * Use the special id of {@literal Map>} - * to receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal ">"} to receive only new messages. * @param group The consumer group name. * @param consumer The newly created consumer. * @return Command Response - A {@literal Map>} with @@ -3867,10 +3861,8 @@ public T xreadgroup( * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. * @see valkey.io for details. - * @param keysAndIds A Map of keys and entry ids to read from. The Map - * is composed of a stream's key and the id of the entry after which the stream will be read. - * Use the special id of {@literal Map>} to - * receive only new messages. + * @param keysAndIds A Map of keys and entry IDs to read from.
+ * Use the special ID of {@literal ">"} to receive only new messages. * @param group The consumer group name. * @param consumer The newly created consumer. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index ecf8bec0ba..ae26a5c985 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -141,6 +141,7 @@ function initialize() { StreamGroupOptions, StreamTrimOptions, StreamAddOptions, + StreamReadGroupOptions, StreamReadOptions, StreamClaimOptions, StreamPendingOptions, @@ -240,6 +241,7 @@ function initialize() { StreamTrimOptions, StreamAddOptions, StreamClaimOptions, + StreamReadGroupOptions, StreamReadOptions, StreamPendingOptions, ScriptOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 2ab9e6d80d..89aeaa52f4 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -50,6 +50,7 @@ import { StreamClaimOptions, StreamGroupOptions, StreamPendingOptions, + StreamReadGroupOptions, StreamReadOptions, StreamTrimOptions, TimeUnit, @@ -185,6 +186,7 @@ import { createXPending, createXRange, createXRead, + createXReadGroup, createXTrim, createZAdd, createZCard, @@ -4355,9 +4357,10 @@ export class BaseClient { * * @see {@link https://valkey.io/commands/xread/|valkey.io} for more details. * - * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. - * @param options - options detailing how to read the stream. - * @returns A map of stream keys, to a map of stream ids, to an array of entries. + * @param keys_and_ids - An object of stream keys and entry IDs to read from. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadOptions}. + * @returns A `Record` of stream keys, each key is mapped to a `Record` of stream ids, to an `Array` of entries. + * * @example * ```typescript * const streamResults = await client.xread({"my_stream": "0-0", "writers": "0-0"}); @@ -4381,6 +4384,50 @@ export class BaseClient { return this.createWritePromise(createXRead(keys_and_ids, options)); } + /** + * Reads entries from the given streams owned by a consumer group. + * + * @see {@link https://valkey.io/commands/xreadgroup/|valkey.io} for details. + * + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param keys_and_ids - An object of stream keys and entry IDs to read from. + * Use the special entry ID of `">"` to receive only new messages. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadGroupOptions}. + * @returns A map of stream keys, each key is mapped to a map of stream ids, which is mapped to an array of entries. + * Returns `null` if there is no stream that can be served. + * + * @example + * ```typescript + * const streamResults = await client.xreadgroup("my_group", "my_consumer", {"my_stream": "0-0", "writers_stream": "0-0", "readers_stream", ">"}); + * console.log(result); // Output: + * // { + * // "my_stream": { + * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], + * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // }, + * // "writers_stream": { + * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], + * // "1526985685298-0": null, // entry was deleted + * // }, + * // "readers_stream": {} // stream is empty + * // } + * ``` + */ + public xreadgroup( + group: string, + consumer: string, + keys_and_ids: Record, + options?: StreamReadGroupOptions, + ): Promise + > | null> { + return this.createWritePromise( + createXReadGroup(group, consumer, keys_and_ids, options), + ); + } + /** * Returns the number of entries in the stream stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 99eddc6767..7953b28f68 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1651,10 +1651,10 @@ function getLexBoundaryArg(score: Boundary | Boundary): string { } if (score.isInclusive == false) { - return "(" + score.value.toString(); + return "(" + score.value; } - return "[" + score.value.toString(); + return "[" + score.value; } /** Returns a string representation of a stream boundary as a command argument. */ @@ -2360,6 +2360,7 @@ export enum FlushMode { ASYNC = "ASYNC", } +/** Optional arguments for {@link BaseClient.xread|xread} command. */ export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or @@ -2374,30 +2375,39 @@ export type StreamReadOptions = { count?: number; }; -function addReadOptions(options: StreamReadOptions, args: string[]) { - if (options.count !== undefined) { +/** Optional arguments for {@link BaseClient.xreadgroup|xreadgroup} command. */ +export type StreamReadGroupOptions = StreamReadOptions & { + /** + * If set, messages are not added to the Pending Entries List (PEL). This is equivalent to + * acknowledging the message when it is read. + */ + noAck?: boolean; +}; + +/** @internal */ +function addReadOptions(options?: StreamReadOptions): string[] { + const args = []; + + if (options?.count !== undefined) { args.push("COUNT"); args.push(options.count.toString()); } - if (options.block !== undefined) { + if (options?.block !== undefined) { args.push("BLOCK"); args.push(options.block.toString()); } -} - -function addStreamsArgs(keys_and_ids: Record, args: string[]) { - args.push("STREAMS"); - - const pairs = Object.entries(keys_and_ids); - for (const [key] of pairs) { - args.push(key); - } + return args; +} - for (const [, id] of pairs) { - args.push(id); - } +/** @internal */ +function addStreamsArgs(keys_and_ids: Record): string[] { + return [ + "STREAMS", + ...Object.keys(keys_and_ids), + ...Object.values(keys_and_ids), + ]; } /** @@ -2407,15 +2417,29 @@ export function createXRead( keys_and_ids: Record, options?: StreamReadOptions, ): command_request.Command { - const args: string[] = []; + const args = addReadOptions(options); + args.push(...addStreamsArgs(keys_and_ids)); + + return createCommand(RequestType.XRead, args); +} + +/** @internal */ +export function createXReadGroup( + group: string, + consumer: string, + keys_and_ids: Record, + options?: StreamReadGroupOptions, +): command_request.Command { + const args: string[] = ["GROUP", group, consumer]; if (options) { - addReadOptions(options, args); + args.push(...addReadOptions(options)); + if (options.noAck) args.push("NOACK"); } - addStreamsArgs(keys_and_ids, args); + args.push(...addStreamsArgs(keys_and_ids)); - return createCommand(RequestType.XRead, args); + return createCommand(RequestType.XReadGroup, args); } /** @@ -2467,11 +2491,11 @@ export function createXLen(key: string): command_request.Command { /** Optional arguments for {@link BaseClient.xpendingWithOptions|xpending}. */ export type StreamPendingOptions = { - /** Filter pending entries by their idle time - in milliseconds */ + /** Filter pending entries by their idle time - in milliseconds. Available since Valkey 6.2.0. */ minIdleTime?: number; - /** Starting stream ID bound for range. */ + /** Starting stream ID bound for range. Exclusive range is available since Valkey 6.2.0. */ start: Boundary; - /** Ending stream ID bound for range. */ + /** Ending stream ID bound for range. Exclusive range is available since Valkey 6.2.0. */ end: Boundary; /** Limit the number of messages returned. */ count: number; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 986aabff9b..af36ab97f0 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -56,6 +56,7 @@ import { StreamClaimOptions, StreamGroupOptions, StreamPendingOptions, + StreamReadGroupOptions, StreamReadOptions, StreamTrimOptions, TimeUnit, @@ -218,6 +219,7 @@ import { createXPending, createXRange, createXRead, + createXReadGroup, createXTrim, createZAdd, createZCard, @@ -2387,12 +2389,13 @@ export class BaseTransaction> { /** * Reads entries from the given streams. + * * @see {@link https://valkey.io/commands/xread/|valkey.io} for details. * - * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. - * @param options - options detailing how to read the stream. + * @param keys_and_ids - An object of stream keys and entry IDs to read from. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadOptions}. * - * Command Response - A map between a stream key, and an array of entries in the matching key. The entries are in an [id, fields[]] format. + * Command Response - A `Record` of stream keys, each key is mapped to a `Record` of stream ids, to an `Array` of entries. */ public xread( keys_and_ids: Record, @@ -2401,6 +2404,31 @@ export class BaseTransaction> { return this.addAndReturn(createXRead(keys_and_ids, options)); } + /** + * Reads entries from the given streams owned by a consumer group. + * + * @see {@link https://valkey.io/commands/xreadgroup/|valkey.io} for details. + * + * @param group - The consumer group name. + * @param consumer - The group consumer. + * @param keys_and_ids - An object of stream keys and entry IDs to read from. + * Use the special ID of `">"` to receive only new messages. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadGroupOptions}. + * + * Command Response - A map of stream keys, each key is mapped to a map of stream ids, which is mapped to an array of entries. + * Returns `null` if there is no stream that can be served. + */ + public xreadgroup( + group: string, + consumer: string, + keys_and_ids: Record, + options?: StreamReadGroupOptions, + ): T { + return this.addAndReturn( + createXReadGroup(group, consumer, keys_and_ids, options), + ); + } + /** * Returns the number of entries in the stream stored at `key`. * diff --git a/node/tests/GlideClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts index 72ee0fe8f1..b2025b0cc8 100644 --- a/node/tests/GlideClientInternals.test.ts +++ b/node/tests/GlideClientInternals.test.ts @@ -311,7 +311,6 @@ describe("SocketConnectionInternals", () => { ); }); const result = await connection.get("foo", Decoder.String); - console.log(result); expect(result).toEqual(expected); }); }; diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 4304a5a988..cadd3e5ad4 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -385,6 +385,8 @@ describe("GlideClusterClient", () => { client.blmpop(["abc", "def"], ListDirection.RIGHT, 0.1, 1), client.bzpopmax(["abc", "def"], 0.5), client.bzpopmin(["abc", "def"], 0.5), + client.xread({ abc: "0-0", zxy: "0-0", lkn: "0-0" }), + client.xreadgroup("_", "_", { abc: ">", zxy: ">", lkn: ">" }), ]; if (gte(cluster.getVersion(), "6.2.0")) { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 8dea7c45d1..f948de2edd 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5509,11 +5509,12 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `streams read test_%p`, - async () => { + `streams xread test_%p`, + async (protocol) => { await runTest(async (client: BaseClient) => { - const key1 = uuidv4(); - const key2 = `{${key1}}${uuidv4()}`; + const key1 = "{xread}-1-" + uuidv4(); + const key2 = "{xread}-2-" + uuidv4(); + const key3 = "{xread}-3-" + uuidv4(); const field1 = "foo"; const field2 = "bar"; const field3 = "barvaz"; @@ -5565,13 +5566,156 @@ export function runBaseTests(config: { }, }; expect(result).toEqual(expected); - }, ProtocolVersion.RESP2); + + // key does not exist + expect(await client.xread({ [key3]: "0-0" })).toBeNull(); + expect( + await client.xread({ + [key2]: timestamp_2_1 as string, + [key3]: "0-0", + }), + ).toEqual({ + [key2]: { + [timestamp_2_2 as string]: [["bar", "bar2"]], + [timestamp_2_3 as string]: [["bar", "bar3"]], + }, + }); + + // key is not a stream + expect(await client.set(key3, uuidv4())).toEqual("OK"); + await expect(client.xread({ [key3]: "0-0" })).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xreadgroup test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{xreadgroup}-1-" + uuidv4(); + const key2 = "{xreadgroup}-2-" + uuidv4(); + const key3 = "{xreadgroup}-3-" + uuidv4(); + const group = uuidv4(); + const consumer = uuidv4(); + + // setup data + expect( + await client.xgroupCreate(key1, group, "0", { + mkStream: true, + }), + ).toEqual("OK"); + + expect( + await client.xgroupCreateConsumer(key1, group, consumer), + ).toBeTruthy(); + + const entry1 = (await client.xadd(key1, [ + ["a", "b"], + ])) as string; + const entry2 = (await client.xadd(key1, [ + ["c", "d"], + ])) as string; + + // read the entire stream for the consumer and mark messages as pending + expect( + await client.xreadgroup(group, consumer, { [key1]: ">" }), + ).toEqual({ + [key1]: { + [entry1]: [["a", "b"]], + [entry2]: [["c", "d"]], + }, + }); + + // delete one of the entries + expect(await client.xdel(key1, [entry1])).toEqual(1); + + // now xreadgroup returns one empty entry and one non-empty entry + expect( + await client.xreadgroup(group, consumer, { [key1]: "0" }), + ).toEqual({ + [key1]: { + [entry1]: null, + [entry2]: [["c", "d"]], + }, + }); + + // try to read new messages only + expect( + await client.xreadgroup(group, consumer, { [key1]: ">" }), + ).toBeNull(); + + // add a message and read it with ">" + const entry3 = (await client.xadd(key1, [ + ["e", "f"], + ])) as string; + expect( + await client.xreadgroup(group, consumer, { [key1]: ">" }), + ).toEqual({ + [key1]: { + [entry3]: [["e", "f"]], + }, + }); + + // add second key with a group and a consumer, but no messages + expect( + await client.xgroupCreate(key2, group, "0", { + mkStream: true, + }), + ).toEqual("OK"); + expect( + await client.xgroupCreateConsumer(key2, group, consumer), + ).toBeTruthy(); + + // read both keys + expect( + await client.xreadgroup(group, consumer, { + [key1]: "0", + [key2]: "0", + }), + ).toEqual({ + [key1]: { + [entry1]: null, + [entry2]: [["c", "d"]], + [entry3]: [["e", "f"]], + }, + [key2]: {}, + }); + + // error cases: + // key does not exist + await expect( + client.xreadgroup("_", "_", { [key3]: "0-0" }), + ).rejects.toThrow(RequestError); + // key is not a stream + expect(await client.set(key3, uuidv4())).toEqual("OK"); + await expect( + client.xreadgroup("_", "_", { [key3]: "0-0" }), + ).rejects.toThrow(RequestError); + expect(await client.del([key3])).toEqual(1); + // group and consumer don't exist + await client.xadd(key3, [["a", "b"]]); + await expect( + client.xreadgroup("_", "_", { [key3]: "0-0" }), + ).rejects.toThrow(RequestError); + // consumer don't exist + expect(await client.xgroupCreate(key3, group, "0-0")).toEqual( + "OK", + ); + expect( + await client.xreadgroup(group, "_", { [key3]: "0-0" }), + ).toEqual({ + [key3]: {}, + }); + }, protocol); }, config.timeout, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `xinfo stream test_%p`, + `xinfo stream xinfosream test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); @@ -5597,17 +5741,9 @@ export function runBaseTests(config: { await client.xgroupCreate(key, groupName, streamId0_0), ).toEqual("OK"); - // TODO: uncomment when XREADGROUP is implemented - // const xreadgroupResult = await client.xreadgroup([[key, ">"]], groupName, consumerName); - await client.customCommand([ - "XREADGROUP", - "GROUP", - groupName, - consumerName, - "STREAMS", - key, - ">", - ]); + await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }); // test xinfoStream base (non-full) case: const result = (await client.xinfoStream(key)) as { @@ -8309,18 +8445,14 @@ export function runBaseTests(config: { expect( await client.xgroupCreate(key, groupName1, "0-0"), ).toEqual("OK"); + expect( - await client.customCommand([ - "XREADGROUP", - "GROUP", + await client.xreadgroup( groupName1, consumer1, - "COUNT", - "1", - "STREAMS", - key, - ">", - ]), + { [key]: ">" }, + { count: 1 }, + ), ).toEqual({ [key]: { [streamId1]: [ @@ -8349,15 +8481,9 @@ export function runBaseTests(config: { ), ).toBeTruthy(); expect( - await client.customCommand([ - "XREADGROUP", - "GROUP", - groupName1, - consumer2, - "STREAMS", - key, - ">", - ]), + await client.xreadgroup(groupName1, consumer2, { + [key]: ">", + }), ).toEqual({ [key]: { [streamId2]: [ @@ -8600,7 +8726,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `xpending test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); const group = uuidv4(); @@ -8638,15 +8764,7 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.customCommand([ - "xreadgroup", - "group", - group, - "consumer", - "STREAMS", - key, - ">", - ]), + await client.xreadgroup(group, "consumer", { [key]: ">" }), ).toEqual({ [key]: { "0-1": [ @@ -8667,12 +8785,22 @@ export function runBaseTests(config: { [["consumer", "2"]], ]); - const result = await client.xpendingWithOptions(key, group, { - start: InfBoundary.NegativeInfinity, - end: InfBoundary.PositiveInfinity, - count: 1, - minIdleTime: 42, - }); + const result = await client.xpendingWithOptions( + key, + group, + cluster.checkIfServerVersionLessThan("6.2.0") + ? { + start: InfBoundary.NegativeInfinity, + end: InfBoundary.PositiveInfinity, + count: 1, + } + : { + start: InfBoundary.NegativeInfinity, + end: InfBoundary.PositiveInfinity, + count: 1, + minIdleTime: 42, + }, + ); result[0][2] = 0; // overwrite msec counter to avoid test flakyness expect(result).toEqual([["0-1", "consumer", 0, 1]]); @@ -8732,15 +8860,7 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.customCommand([ - "xreadgroup", - "group", - group, - "consumer", - "STREAMS", - key, - ">", - ]), + await client.xreadgroup(group, "consumer", { [key]: ">" }), ).toEqual({ [key]: { "0-1": [ @@ -8846,15 +8966,7 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.customCommand([ - "xreadgroup", - "group", - group, - "consumer", - "STREAMS", - key, - ">", - ]), + await client.xreadgroup(group, "consumer", { [key]: ">" }), ).toEqual({ [key]: { "0-1": [ @@ -9114,15 +9226,9 @@ export function runBaseTests(config: { // read the entire stream for the consumer and mark messages as pending expect( - await client.customCommand([ - "XREADGROUP", - "GROUP", - groupName, - consumer, - "STREAMS", - key, - ">", - ]), + await client.xreadgroup(groupName, consumer, { + [key]: ">", + }), ).toEqual({ [key]: { [streamid1 as string]: [["field1", "value1"]], diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 29147cdac4..08c56c85d9 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1122,17 +1122,9 @@ export async function transactionTest( "xgroupCreateConsumer(key9, groupName1, consumer)", true, ]); - baseTransaction.customCommand([ - "xreadgroup", - "group", - groupName1, - consumer, - "STREAMS", - key9, - ">", - ]); + baseTransaction.xreadgroup(groupName1, consumer, { [key9]: ">" }); responseData.push([ - "xreadgroup(groupName1, consumer, key9, >)", + 'xreadgroup(groupName1, consumer, {[key9]: ">"})', { [key9]: { "0-2": [["field", "value2"]] } }, ]); baseTransaction.xpending(key9, groupName1); diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index d31a1a428c..c3bb4e1aea 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -2849,8 +2849,7 @@ async def xread( When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. Args: - keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of keys and entry IDs to read from. The mapping is composed of a - stream's key and the ID of the entry after which the stream will be read. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of keys and entry IDs to read from. options (Optional[StreamReadOptions]): Options detailing how to read the stream. Returns: @@ -3053,9 +3052,8 @@ async def xreadgroup( When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. Args: - keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. The special ">" - ID returns messages that were never delivered to any other consumer. Any other valid ID will return - entries pending for the consumer with IDs greater than the one provided. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. + Use the special entry ID of `">"` to receive only new messages. group_name (TEncodable): The consumer group name. consumer_name (TEncodable): The consumer name. The consumer will be auto-created if it does not already exist. options (Optional[StreamReadGroupOptions]): Options detailing how to read the stream. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index dda2d58814..3a03351224 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -2299,8 +2299,7 @@ def xread( See https://valkey.io/commands/xread for more details. Args: - keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of keys and entry IDs to read from. The mapping is composed of a - stream's key and the ID of the entry after which the stream will be read. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. options (Optional[StreamReadOptions]): Options detailing how to read the stream. Command response: @@ -2450,9 +2449,8 @@ def xreadgroup( See https://valkey.io/commands/xreadgroup for more details. Args: - keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. The special ">" - ID returns messages that were never delivered to any other consumer. Any other valid ID will return - entries pending for the consumer with IDs greater than the one provided. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. + Use the special entry ID of `">"` to receive only new messages. group_name (TEncodable): The consumer group name. consumer_name (TEncodable): The consumer name. The consumer will be auto-created if it does not already exist. options (Optional[StreamReadGroupOptions]): Options detailing how to read the stream. From 6d7658d7db327a336b642adca953c61fa177c10e Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:25:17 -0700 Subject: [PATCH 188/236] Node: Add XREVRANGE command (#2148) * Implement XREVRANGE command Signed-off-by: Jonathan Louie * Add changelog entry for XREVRANGE Signed-off-by: Jonathan Louie * Update CHANGELOG.md Signed-off-by: Jonathan Louie * Update docs and try to fix tests Signed-off-by: Jonathan Louie * Fix xrevrange test Signed-off-by: Jonathan Louie * Revert changes to rejects tests Signed-off-by: Jonathan Louie --------- Signed-off-by: Jonathan Louie Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 43 ++++++++++++++++++- node/src/Commands.ts | 19 +++++++++ node/src/Transaction.ts | 32 +++++++++++++- node/tests/SharedTests.ts | 84 ++++++++++++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 2 + 6 files changed, 177 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba3fbafc7..2bec770dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added XRANGE command ([#2069](https://github.com/valkey-io/valkey-glide/pull/2069)) +* Node: Added XREVRANGE command ([#2148](https://github.com/valkey-io/valkey-glide/pull/2148)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added WAIT command ([#2113](https://github.com/valkey-io/valkey-glide/pull/2113)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 89aeaa52f4..3ec3081490 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -187,6 +187,7 @@ import { createXRange, createXRead, createXReadGroup, + createXRevRange, createXTrim, createZAdd, createZCard, @@ -3247,7 +3248,7 @@ export class BaseClient { * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. * @param count - An optional argument specifying the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. - * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is negative. + * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. * * @example * ```typescript @@ -3270,6 +3271,46 @@ export class BaseClient { return this.createWritePromise(createXRange(key, start, end, count)); } + /** + * Returns stream entries matching a given range of entry IDs in reverse order. Equivalent to {@link xrange} but returns the + * entries in reverse order. + * + * @see {@link https://valkey.io/commands/xrevrange/|valkey.io} for more details. + * + * @param key - The key of the stream. + * @param end - The ending stream entry ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. + * @param start - The ending stream ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.NegativeInfinity` to start with the minimum available ID. + * @param count - An optional argument specifying the maximum count of stream entries to return. + * If `count` is not provided, all stream entries in the range will be returned. + * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + * + * @example + * ```typescript + * await client.xadd("mystream", [["field1", "value1"]], {id: "0-1"}); + * await client.xadd("mystream", [["field2", "value2"], ["field2", "value3"]], {id: "0-2"}); + * console.log(await client.xrevrange("mystream", InfBoundary.PositiveInfinity, InfBoundary.NegativeInfinity)); + * // Output: + * // { + * // "0-2": [["field2", "value2"], ["field2", "value3"]], + * // "0-1": [["field1", "value1"]], + * // } // Indicates the stream entry IDs and their associated field-value pairs for all stream entries in "mystream". + * ``` + */ + public async xrevrange( + key: string, + end: Boundary, + start: Boundary, + count?: number, + ): Promise | null> { + return this.createWritePromise(createXRevRange(key, end, start, count)); + } + /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7953b28f68..dbe2ee674a 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2081,6 +2081,25 @@ export function createXRange( return createCommand(RequestType.XRange, args); } +/** + * @internal + */ +export function createXRevRange( + key: string, + start: Boundary, + end: Boundary, + count?: number, +): command_request.Command { + const args = [key, getStreamBoundaryArg(start), getStreamBoundaryArg(end)]; + + if (count !== undefined) { + args.push("COUNT"); + args.push(count.toString()); + } + + return createCommand(RequestType.XRevRange, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index af36ab97f0..794264fc20 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -220,6 +220,7 @@ import { createXRange, createXRead, createXReadGroup, + createXRevRange, createXTrim, createZAdd, createZCard, @@ -2376,7 +2377,7 @@ export class BaseTransaction> { * @param count - An optional argument specifying the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. * - * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is negative. + * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. */ public xrange( key: string, @@ -2387,6 +2388,35 @@ export class BaseTransaction> { return this.addAndReturn(createXRange(key, start, end, count)); } + /** + * Returns stream entries matching a given range of entry IDs in reverse order. Equivalent to {@link xrange} but returns the + * entries in reverse order. + * + * @see {@link https://valkey.io/commands/xrevrange/|valkey.io} for more details. + * + * @param key - The key of the stream. + * @param end - The ending stream entry ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. + * @param start - The ending stream ID bound for the range. + * - Use `value` to specify a stream entry ID. + * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. + * - Use `InfBoundary.NegativeInfinity` to start with the minimum available ID. + * @param count - An optional argument specifying the maximum count of stream entries to return. + * If `count` is not provided, all stream entries in the range will be returned. + * + * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + */ + public xrevrange( + key: string, + end: Boundary, + start: Boundary, + count?: number, + ): T { + return this.addAndReturn(createXRevRange(key, end, start, count)); + } + /** * Reads entries from the given streams. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index f948de2edd..82797d3ea1 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5211,7 +5211,7 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `xrange test_%p`, + `xrange and xrevrange test_%p`, async (protocol) => { await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); @@ -5241,6 +5241,17 @@ export function runBaseTests(config: { [streamId2]: [["f2", "v2"]], }); + expect( + await client.xrevrange( + key, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + ), + ).toEqual({ + [streamId2]: [["f2", "v2"]], + [streamId1]: [["f1", "v1"]], + }); + // returns empty mapping if + before - expect( await client.xrange( @@ -5249,6 +5260,14 @@ export function runBaseTests(config: { InfBoundary.NegativeInfinity, ), ).toEqual({}); + // rev search returns empty mapping if - before + + expect( + await client.xrevrange( + key, + InfBoundary.NegativeInfinity, + InfBoundary.PositiveInfinity, + ), + ).toEqual({}); expect( await client.xadd(key, [["f3", "v3"]], { id: streamId3 }), @@ -5264,9 +5283,18 @@ export function runBaseTests(config: { 1, ), ).toEqual({ [streamId3]: [["f3", "v3"]] }); + + expect( + await client.xrevrange( + key, + { value: "5" }, + { isInclusive: false, value: streamId2 }, + 1, + ), + ).toEqual({ [streamId3]: [["f3", "v3"]] }); } - // xrange against an emptied stream + // xrange/xrevrange against an emptied stream expect( await client.xdel(key, [streamId1, streamId2, streamId3]), ).toEqual(3); @@ -5278,6 +5306,14 @@ export function runBaseTests(config: { 10, ), ).toEqual({}); + expect( + await client.xrevrange( + key, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + 10, + ), + ).toEqual({}); expect( await client.xrange( @@ -5286,6 +5322,13 @@ export function runBaseTests(config: { InfBoundary.PositiveInfinity, ), ).toEqual({}); + expect( + await client.xrevrange( + nonExistingKey, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + ), + ).toEqual({}); // count value < 1 returns null expect( @@ -5304,6 +5347,22 @@ export function runBaseTests(config: { -1, ), ).toEqual(null); + expect( + await client.xrevrange( + key, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + 0, + ), + ).toEqual(null); + expect( + await client.xrevrange( + key, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + -1, + ), + ).toEqual(null); // key exists, but it is not a stream expect(await client.set(stringKey, "foo")); @@ -5314,6 +5373,13 @@ export function runBaseTests(config: { InfBoundary.PositiveInfinity, ), ).rejects.toThrow(RequestError); + await expect( + client.xrevrange( + stringKey, + InfBoundary.PositiveInfinity, + InfBoundary.NegativeInfinity, + ), + ).rejects.toThrow(RequestError); // invalid start bound await expect( @@ -5323,6 +5389,11 @@ export function runBaseTests(config: { InfBoundary.PositiveInfinity, ), ).rejects.toThrow(RequestError); + await expect( + client.xrevrange(key, InfBoundary.PositiveInfinity, { + value: "not_a_stream_id", + }), + ).rejects.toThrow(RequestError); // invalid end bound await expect( @@ -5330,6 +5401,15 @@ export function runBaseTests(config: { value: "not_a_stream_id", }), ).rejects.toThrow(RequestError); + await expect( + client.xrevrange( + key, + { + value: "not_a_stream_id", + }, + InfBoundary.NegativeInfinity, + ), + ).rejects.toThrow(RequestError); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 08c56c85d9..b5ae554dc7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1082,6 +1082,8 @@ export async function transactionTest( responseData.push(["xlen(key9)", 3]); baseTransaction.xrange(key9, { value: "0-1" }, { value: "0-1" }); responseData.push(["xrange(key9)", { "0-1": [["field", "value1"]] }]); + baseTransaction.xrevrange(key9, { value: "0-1" }, { value: "0-1" }); + responseData.push(["xrevrange(key9)", { "0-1": [["field", "value1"]] }]); baseTransaction.xread({ [key9]: "0-1" }); responseData.push([ 'xread({ [key9]: "0-1" })', From ddb5af9dfe829e9f557543ff70ade04791057c83 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:46:30 -0700 Subject: [PATCH 189/236] Node: add ZUNIONSTORE (#2145) * implement zunionstore Signed-off-by: Chloe Yip * add changelog Signed-off-by: Chloe Yip * implement zunionstore Signed-off-by: Chloe Yip * add changelog Signed-off-by: Chloe Yip * address comments Signed-off-by: Chloe Yip * delete glide-for-redis submodule package Signed-off-by: Chloe Yip * add cluster example Signed-off-by: Chloe Yip * add remarks to base client Signed-off-by: Chloe Yip * fix test Signed-off-by: Chloe Yip * Apply suggestions from code review Co-authored-by: Yury-Fridlyand Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --------- Signed-off-by: Chloe Yip Signed-off-by: Chloe Yip <168601573+cyip10@users.noreply.github.com> Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Co-authored-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Co-authored-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 37 +++++ node/src/Commands.ts | 12 ++ node/src/Transaction.ts | 25 ++++ node/tests/GlideClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 190 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 7 files changed, 268 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bec770dc5..363dbbd4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) * Java: Added PUBSUB CHANNELS, NUMPAT and NUMSUB commands ([#2105](https://github.com/valkey-io/valkey-glide/pull/2105)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 3ec3081490..8160c81f8d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -216,6 +216,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnionStore, } from "./Commands"; import { ClosingError, @@ -3579,6 +3580,42 @@ export class BaseClient { return this.createWritePromise(createZScore(key, member)); } + /** + * Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * To get the result directly, see {@link zunionWithScores}. + * + * @see {@link https://valkey.io/commands/zunionstore/|valkey.io} for details. + * @remarks When in cluster mode, `destination` and all keys in `keys` both must map to the same hash slot. + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * @returns The number of elements in the resulting sorted set stored at `destination`. + * + * * @example + * ```typescript + * // Example usage of zunionstore command with an existing key + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + * await client.zadd("key2", {"member1": 9.5}) + * await client.zunionstore("my_sorted_set", ["key1", "key2"]) // Output: 2 - Indicates that the sorted set "my_sorted_set" contains two elements. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20, 'member2': 8.2} - "member1" is now stored in "my_sorted_set" with score of 20 and "member2" with score of 8.2. + * await client.zunionstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 2 - Indicates that the sorted set "my_sorted_set" contains two elements, and each score is the maximum score between the sets. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5, 'member2': 8.2} - "member1" is now stored in "my_sorted_set" with score of 10.5 and "member2" with score of 8.2. + * await client.zunionstore("my_sorted_set", ["key1, "key2], {weights: [2, 1]}) // Output: 46 + * ``` + */ + public async zunionstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise { + return this.createWritePromise( + createZUnionStore(destination, keys, aggregationType), + ); + } + /** * Returns the scores associated with the specified `members` in the sorted set stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index dbe2ee674a..4947164109 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1532,6 +1532,18 @@ export function createZScore( return createCommand(RequestType.ZScore, [key, member]); } +/** + * @internal + */ +export function createZUnionStore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): command_request.Command { + const args = createZCmdStoreArgs(destination, keys, aggregationType); + return createCommand(RequestType.ZUnionStore, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 794264fc20..5f0a7b7bfb 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -249,6 +249,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnionStore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1769,6 +1770,30 @@ export class BaseTransaction> { return this.addAndReturn(createZScore(key, member)); } + /** + * Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * To get the result directly, see {@link zunionWithScores}. + * + * @see {@link https://valkey.io/commands/zunionstore/|valkey.io} for details. + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * + * Command Response - The number of elements in the resulting sorted set stored at `destination`. + */ + public zunionstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn( + createZUnionStore(destination, keys, aggregationType), + ); + } + /** * Returns the scores associated with the specified `members` in the sorted set stored at `key`. * diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index cadd3e5ad4..365f4b5cb6 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -373,6 +373,7 @@ describe("GlideClusterClient", () => { client.sinter(["abc", "zxy", "lkn"]), client.sinterstore("abc", ["zxy", "lkn"]), client.zinterstore("abc", ["zxy", "lkn"]), + client.zunionstore("abc", ["zxy", "lkn"]), client.sunionstore("abc", ["zxy", "lkn"]), client.sunion(["abc", "zxy", "lkn"]), client.pfcount(["abc", "zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 82797d3ea1..4428f6018b 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3858,6 +3858,196 @@ export function runBaseTests(config: { config.timeout, ); + // ZUnionStore command tests + async function zunionStoreWithMaxAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MAX score of elements + expect(await client.zunionstore(key3, [key1, key2], "MAX")).toEqual(3); + const zunionstoreMapMax = await client.zrangeWithScores(key3, range); + const expectedMapMax = { + one: 1.5, + two: 2.5, + three: 3.5, + }; + expect(zunionstoreMapMax).toEqual(expectedMapMax); + } + + async function zunionStoreWithMinAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MIN score of elements + expect(await client.zunionstore(key3, [key1, key2], "MIN")).toEqual(3); + const zunionstoreMapMin = await client.zrangeWithScores(key3, range); + const expectedMapMin = { + one: 1.0, + two: 2.0, + three: 3.5, + }; + expect(zunionstoreMapMin).toEqual(expectedMapMin); + } + + async function zunionStoreWithSumAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the SUM score of elements + expect(await client.zunionstore(key3, [key1, key2], "SUM")).toEqual(3); + const zunionstoreMapSum = await client.zrangeWithScores(key3, range); + const expectedMapSum = { + one: 2.5, + two: 4.5, + three: 3.5, + }; + expect(zunionstoreMapSum).toEqual(expectedMapSum); + } + + async function zunionStoreBasicTest(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + expect(await client.zunionstore(key3, [key1, key2])).toEqual(3); + const zunionstoreMap = await client.zrangeWithScores(key3, range); + const expectedMap = { + one: 3.0, + three: 4.0, + two: 5.0, + }; + expect(zunionstoreMap).toEqual(expectedMap); + } + + async function zunionStoreWithWeightsAndAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + expect( + await client.zunionstore( + key3, + [ + [key1, 2.0], + [key2, 2.0], + ], + "SUM", + ), + ).toEqual(3); + const zunionstoreMapMultiplied = await client.zrangeWithScores( + key3, + range, + ); + const expectedMapMultiplied = { + one: 5.0, + three: 7.0, + two: 9.0, + }; + expect(zunionstoreMapMultiplied).toEqual(expectedMapMultiplied); + } + + async function zunionStoreEmptyCases(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + const membersScores1 = { one: 1.0, two: 2.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + + // Non existing key + expect( + await client.zunionstore(key2, [ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(2); + + const zunionstore_map_nonexistingkey = await client.zrangeWithScores( + key2, + range, + ); + + const expectedMapMultiplied = { + one: 1.0, + two: 2.0, + }; + expect(zunionstore_map_nonexistingkey).toEqual(expectedMapMultiplied); + + // Empty list check + await expect(client.zunionstore("{xyz}", [])).rejects.toThrow(); + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunionstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + await zunionStoreBasicTest(client); + await zunionStoreWithMaxAggregation(client); + await zunionStoreWithMinAggregation(client); + await zunionStoreWithSumAggregation(client); + await zunionStoreWithWeightsAndAggregation(client); + await zunionStoreEmptyCases(client); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zmscore test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index b5ae554dc7..a987b285a5 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -988,6 +988,8 @@ export async function transactionTest( responseData.push(["zdiffWithScores([key13, key12])", { three: 3.5 }]); baseTransaction.zdiffstore(key13, [key13, key13]); responseData.push(["zdiffstore(key13, [key13, key13])", 0]); + baseTransaction.zunionstore(key5, [key12, key13]); + responseData.push(["zunionstore(key5, [key12, key13])", 2]); baseTransaction.zmscore(key12, ["two", "one"]); responseData.push(['zmscore(key12, ["two", "one"]', [2.0, 1.0]]); baseTransaction.zinterstore(key12, [key12, key13]); From 89b10d41cf5b5c374fafe7e8ec29ea18ac97719d Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 19 Aug 2024 12:49:17 -0700 Subject: [PATCH 190/236] Node: Add `FUNCTION DUMP` and `FUNCTION RESTORE` commands. (#2129) * Add `FUNCTION DUMP` and `FUNCTION RESTORE` commands. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/Commands.ts | 38 +++++ node/src/GlideClient.ts | 74 ++++++++-- node/src/GlideClusterClient.ts | 89 ++++++++++-- node/src/Transaction.ts | 1 + node/tests/AsyncClient.test.ts | 4 +- node/tests/GlideClient.test.ts | 148 ++++++++++++++++--- node/tests/GlideClusterClient.test.ts | 198 ++++++++++++++++++++++++-- node/tests/SharedTests.ts | 31 ++-- node/tests/TestUtilities.ts | 8 +- 11 files changed, 509 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363dbbd4ea..c8dba939e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index ae26a5c985..276ebfa0b4 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -108,6 +108,7 @@ function initialize() { FunctionListOptions, FunctionListResponse, FunctionStatsResponse, + FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, TimeUnit, @@ -205,6 +206,7 @@ function initialize() { FunctionListOptions, FunctionListResponse, FunctionStatsResponse, + FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, StreamEntries, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 4947164109..f5e5ac55a8 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2293,6 +2293,44 @@ export function createFunctionKill(): command_request.Command { return createCommand(RequestType.FunctionKill, []); } +/** @internal */ +export function createFunctionDump(): command_request.Command { + return createCommand(RequestType.FunctionDump, []); +} + +/** + * Option for `FUNCTION RESTORE` command: {@link GlideClient.functionRestore} and + * {@link GlideClusterClient.functionRestore}. + * + * @see {@link https://valkey.io/commands/function-restore/"|valkey.io} for more details. + */ +export enum FunctionRestorePolicy { + /** + * Appends the restored libraries to the existing libraries and aborts on collision. This is the + * default policy. + */ + APPEND = "APPEND", + /** Deletes all existing libraries before restoring the payload. */ + FLUSH = "FLUSH", + /** + * Appends the restored libraries to the existing libraries, replacing any existing ones in case + * of name collisions. Note that this policy doesn't prevent function name collisions, only + * libraries. + */ + REPLACE = "REPLACE", +} + +/** @internal */ +export function createFunctionRestore( + data: Buffer, + policy?: FunctionRestorePolicy, +): command_request.Command { + return createCommand( + RequestType.FunctionRestore, + policy ? [data, policy] : [data], + ); +} + /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index e63f7f8fb1..e5bfb9a1b4 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -16,6 +16,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionRestorePolicy, FunctionStatsResponse, InfoOptions, LolwutOptions, @@ -33,10 +34,12 @@ import { createFlushAll, createFlushDB, createFunctionDelete, + createFunctionDump, createFunctionFlush, createFunctionKill, createFunctionList, createFunctionLoad, + createFunctionRestore, createFunctionStats, createInfo, createLastSave, @@ -174,26 +177,27 @@ export class GlideClient extends BaseClient { * * @see {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#transaction|Valkey Glide Wiki} for details on Valkey Transactions. * - * @param transaction - A Transaction object containing a list of commands to be executed. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the responses. If not set, the default decoder from the client config will be used. + * @param transaction - A {@link Transaction} object containing a list of commands to be executed. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of results corresponding to the execution of each command in the transaction. - * If a command returns a value, it will be included in the list. If a command doesn't return a value, - * the list entry will be null. - * If the transaction failed due to a WATCH command, `exec` will return `null`. + * If a command returns a value, it will be included in the list. If a command doesn't return a value, + * the list entry will be `null`. + * If the transaction failed due to a `WATCH` command, `exec` will return `null`. */ public async exec( transaction: Transaction, - decoder: Decoder = this.defaultDecoder, + decoder?: Decoder, ): Promise { return this.createWritePromise( transaction.commands, { decoder: decoder }, - ).then((result: ReturnType[] | null) => { - return this.processResultWithSetCommands( + ).then((result) => + this.processResultWithSetCommands( result, transaction.setCommandsIndexes, - ); - }); + ), + ); } /** Executes a single command, without checking inputs. Every part of the command, including subcommands, @@ -643,9 +647,8 @@ export class GlideClient extends BaseClient { * Kills a function that is currently executing. * `FUNCTION KILL` terminates read-only functions only. * - * See https://valkey.io/commands/function-kill/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-kill/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @returns `OK` if function is terminated. Otherwise, throws an error. * @example @@ -657,6 +660,51 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionKill()); } + /** + * Returns the serialized payload of all loaded libraries. + * + * @see {@link https://valkey.io/commands/function-dump/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @returns The serialized payload of all loaded libraries. + * + * @example + * ```typescript + * const data = await client.functionDump(); + * // data can be used to restore loaded functions on any Valkey instance + * ``` + */ + public async functionDump(): Promise { + return this.createWritePromise(createFunctionDump(), { + decoder: Decoder.Bytes, + }); + } + + /** + * Restores libraries from the serialized payload returned by {@link functionDump}. + * + * @see {@link https://valkey.io/commands/function-restore/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param payload - The serialized data from {@link functionDump}. + * @param policy - (Optional) A policy for handling existing libraries, see {@link FunctionRestorePolicy}. + * {@link FunctionRestorePolicy.APPEND} is used by default. + * @returns `"OK"`. + * + * @example + * ```typescript + * await client.functionRestore(data, FunctionRestorePolicy.FLUSH); + * ``` + */ + public async functionRestore( + payload: Buffer, + policy?: FunctionRestorePolicy, + ): Promise<"OK"> { + return this.createWritePromise(createFunctionRestore(payload, policy), { + decoder: Decoder.String, + }); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 0aeb430ae4..f4f36e51da 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -16,6 +16,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionRestorePolicy, FunctionStatsResponse, InfoOptions, LolwutOptions, @@ -35,10 +36,12 @@ import { createFlushAll, createFlushDB, createFunctionDelete, + createFunctionDump, createFunctionFlush, createFunctionKill, createFunctionList, createFunctionLoad, + createFunctionRestore, createFunctionStats, createInfo, createLastSave, @@ -368,16 +371,19 @@ export class GlideClusterClient extends BaseClient { /** * Execute a transaction by processing the queued commands. - * @see {@link https://redis.io/topics/Transactions/|Valkey Glide Wiki} for details on Redis Transactions. * - * @param transaction - A ClusterTransaction object containing a list of commands to be executed. - * @param route - If `route` is not provided, the transaction will be routed to the slot owner of the first key found in the transaction. - * If no key is found, the command will be sent to a random node. - * If `route` is provided, the client will route the command to the nodes defined by `route`. + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#transaction|Valkey Glide Wiki} for details on Valkey Transactions. + * + * @param transaction - A {@link ClusterTransaction} object containing a list of commands to be executed. + * @param route - (Optional) If `route` is not provided, the transaction will be routed to the slot owner of the first key found in the transaction. + * If no key is found, the command will be sent to a random node. + * If `route` is provided, the client will route the command to the nodes defined by `route`. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of results corresponding to the execution of each command in the transaction. - * If a command returns a value, it will be included in the list. If a command doesn't return a value, - * the list entry will be null. - * If the transaction failed due to a WATCH command, `exec` will return `null`. + * If a command returns a value, it will be included in the list. If a command doesn't return a value, + * the list entry will be `null`. + * If the transaction failed due to a `WATCH` command, `exec` will return `null`. */ public async exec( transaction: ClusterTransaction, @@ -392,12 +398,12 @@ export class GlideClusterClient extends BaseClient { route: toProtobufRoute(options?.route), decoder: options?.decoder, }, - ).then((result: ReturnType[] | null) => { - return this.processResultWithSetCommands( + ).then((result) => + this.processResultWithSetCommands( result, transaction.setCommandsIndexes, - ); - }); + ), + ); } /** Ping the Redis server. @@ -748,7 +754,7 @@ export class GlideClusterClient extends BaseClient { func: string, args: string[], route?: Routes, - ): Promise { + ): Promise> { return this.createWritePromise(createFCall(func, [], args), { route: toProtobufRoute(route), }); @@ -777,7 +783,7 @@ export class GlideClusterClient extends BaseClient { func: string, args: string[], route?: Routes, - ): Promise { + ): Promise> { return this.createWritePromise(createFCallReadOnly(func, [], args), { route: toProtobufRoute(route), }); @@ -874,7 +880,7 @@ export class GlideClusterClient extends BaseClient { * * @param options - Parameters to filter and request additional info. * @param route - The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed to a random route. + * If not defined, the command will be routed to a random node. * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. * * @example @@ -983,6 +989,59 @@ export class GlideClusterClient extends BaseClient { }); } + /** + * Returns the serialized payload of all loaded libraries. + * + * @see {@link https://valkey.io/commands/function-dump/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param route - (Optional) The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed a random node. + * @returns The serialized payload of all loaded libraries. + * + * @example + * ```typescript + * const data = await client.functionDump(); + * // data can be used to restore loaded functions on any Valkey instance + * ``` + */ + public async functionDump( + route?: Routes, + ): Promise> { + return this.createWritePromise(createFunctionDump(), { + decoder: Decoder.Bytes, + route: toProtobufRoute(route), + }); + } + + /** + * Restores libraries from the serialized payload returned by {@link functionDump}. + * + * @see {@link https://valkey.io/commands/function-restore/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param payload - The serialized data from {@link functionDump}. + * @param policy - (Optional) A policy for handling existing libraries, see {@link FunctionRestorePolicy}. + * {@link FunctionRestorePolicy.APPEND} is used by default. + * @param route - (Optional) The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed all primary nodes. + * @returns `"OK"`. + * + * @example + * ```typescript + * await client.functionRestore(data, { policy: FunctionRestorePolicy.FLUSH, route: "allPrimaries" }); + * ``` + */ + public async functionRestore( + payload: Buffer, + options?: { policy?: FunctionRestorePolicy; route?: Routes }, + ): Promise<"OK"> { + return this.createWritePromise( + createFunctionRestore(payload, options?.policy), + { decoder: Decoder.String, route: toProtobufRoute(options?.route) }, + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 5f0a7b7bfb..dc168f95b1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars + Decoder, // eslint-disable-line @typescript-eslint/no-unused-vars GlideString, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; diff --git a/node/tests/AsyncClient.test.ts b/node/tests/AsyncClient.test.ts index 762e33fede..6d0d3b4246 100644 --- a/node/tests/AsyncClient.test.ts +++ b/node/tests/AsyncClient.test.ts @@ -12,8 +12,6 @@ const FreePort = require("find-free-port"); const PORT_NUMBER = 4000; -type EmptyObject = Record; - describe("AsyncClient", () => { let server: RedisServer; let port: number; @@ -41,7 +39,7 @@ describe("AsyncClient", () => { server.close(); }); - runCommonTests({ + runCommonTests({ init: async () => { const client = await AsyncClient.CreateConnection( "redis://localhost:" + port, diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 48d1035603..f5526104bb 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -21,7 +21,11 @@ import { Transaction, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; +import { + FlushMode, + FunctionRestorePolicy, + SortOrder, +} from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { @@ -41,12 +45,6 @@ import { waitForNotBusy, } from "./TestUtilities"; -/* eslint-disable @typescript-eslint/no-var-requires */ - -type Context = { - client: GlideClient; -}; - const TIMEOUT = 50000; describe("GlideClient", () => { @@ -136,11 +134,9 @@ describe("GlideClient", () => { "check that blocking commands returns never timeout_%p", async (protocol) => { client = await GlideClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - 300, - ), + getClientConfigurationOption(cluster.getAddresses(), protocol, { + requestTimeout: 300, + }), ); const promiseList = [ @@ -198,6 +194,7 @@ describe("GlideClient", () => { expect(await client.dbsize()).toBeGreaterThan(0); expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); expect(await client.dbsize()).toEqual(0); + client.close(); }, ); @@ -221,6 +218,7 @@ describe("GlideClient", () => { expect(await client.get(key)).toEqual(valueEncoded); expect(await client.get(key, Decoder.String)).toEqual(value); expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + client.close(); }, ); @@ -244,6 +242,7 @@ describe("GlideClient", () => { expect(await client.get(key)).toEqual(value); expect(await client.get(key, Decoder.String)).toEqual(value); expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + client.close(); }, ); @@ -263,6 +262,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -279,6 +279,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -302,6 +303,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -326,6 +328,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -840,7 +843,7 @@ describe("GlideClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClient.createClient(config); const testClient = await GlideClient.createClient(config); @@ -911,7 +914,7 @@ describe("GlideClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClient.createClient(config); const testClient = await GlideClient.createClient(config); @@ -981,6 +984,107 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function dump function restore %p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = await GlideClient.createClient(config); + expect(await client.functionFlush()).toEqual("OK"); + + try { + // dumping an empty lib + expect( + (await client.functionDump()).byteLength, + ).toBeGreaterThan(0); + + const name1 = "Foster"; + const name2 = "Dogster"; + // function $name1 returns first argument + // function $name2 returns argument array len + let code = generateLuaLibCode( + name1, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect(await client.functionLoad(code)).toEqual(name1); + + const flist = await client.functionList({ withCode: true }); + const dump = await client.functionDump(); + + // restore without cleaning the lib and/or overwrite option causes an error + await expect(client.functionRestore(dump)).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // APPEND policy also fails for the same reason (name collision) + await expect( + client.functionRestore(dump, FunctionRestorePolicy.APPEND), + ).rejects.toThrow(`Library ${name1} already exists`); + + // REPLACE policy succeeds + expect( + await client.functionRestore( + dump, + FunctionRestorePolicy.REPLACE, + ), + ).toEqual("OK"); + // but nothing changed - all code overwritten + expect(await client.functionList({ withCode: true })).toEqual( + flist, + ); + + // create lib with another name, but with the same function names + expect(await client.functionFlush(FlushMode.SYNC)).toEqual( + "OK", + ); + code = generateLuaLibCode( + name2, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect(await client.functionLoad(code)).toEqual(name2); + + // REPLACE policy now fails due to a name collision + await expect(client.functionRestore(dump)).rejects.toThrow( + new RegExp(`Function ${name1}|${name2} already exists`), + ); + + // FLUSH policy succeeds, but deletes the second lib + expect( + await client.functionRestore( + dump, + FunctionRestorePolicy.FLUSH, + ), + ).toEqual("OK"); + expect(await client.functionList({ withCode: true })).toEqual( + flist, + ); + + // call restored functions + expect(await client.fcall(name1, [], ["meow", "woem"])).toEqual( + "meow", + ); + expect(await client.fcall(name2, [], ["meow", "woem"])).toEqual( + 2, + ); + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", async (protocol) => { @@ -1396,19 +1500,19 @@ describe("GlideClient", () => { TIMEOUT, ); - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( + runBaseTests({ + init: async (protocol, configOverrides) => { + const config = getClientConfigurationOption( cluster.getAddresses(), protocol, + configOverrides, ); - options.protocol = protocol; - options.clientName = clientName; + testsFailed += 1; - client = await GlideClient.createClient(options); - return { client, context: { client }, cluster }; + client = await GlideClient.createClient(config); + return { client, cluster }; }, - close: (context: Context, testSucceeded: boolean) => { + close: (testSucceeded: boolean) => { if (testSucceeded) { testsFailed -= 1; } diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 365f4b5cb6..65d39b1703 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -22,12 +22,14 @@ import { ListDirection, ProtocolVersion, RequestError, + ReturnType, Routes, ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode, + FunctionRestorePolicy, FunctionStatsResponse, GeoUnit, SortOrder, @@ -50,9 +52,6 @@ import { validateTransactionResponse, waitForNotBusy, } from "./TestUtilities"; -type Context = { - client: GlideClusterClient; -}; const TIMEOUT = 50000; @@ -81,25 +80,22 @@ describe("GlideClusterClient", () => { } }); - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( + runBaseTests({ + init: async (protocol, configOverrides) => { + const config = getClientConfigurationOption( cluster.getAddresses(), protocol, + configOverrides, ); - options.protocol = protocol; - options.clientName = clientName; + testsFailed += 1; - client = await GlideClusterClient.createClient(options); + client = await GlideClusterClient.createClient(config); return { - context: { - client, - }, client, cluster, }; }, - close: (context: Context, testSucceeded: boolean) => { + close: (testSucceeded: boolean) => { if (testSucceeded) { testsFailed -= 1; } @@ -1171,7 +1167,7 @@ describe("GlideClusterClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClusterClient.createClient(config); @@ -1262,6 +1258,178 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + + it("function dump function restore %p", async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = + await GlideClusterClient.createClient(config); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + expect( + await client.functionFlush(FlushMode.SYNC, route), + ).toEqual("OK"); + + try { + // dumping an empty lib + let response = await client.functionDump(route); + + if (singleNodeRoute) { + expect(response.byteLength).toBeGreaterThan(0); + } else { + Object.values(response).forEach((d: Buffer) => + expect(d.byteLength).toBeGreaterThan(0), + ); + } + + const name1 = "Foster"; + const name2 = "Dogster"; + // function $name1 returns first argument + // function $name2 returns argument array len + let code = generateLuaLibCode( + name1, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(name1); + + const flist = await client.functionList( + { withCode: true }, + route, + ); + response = await client.functionDump(route); + const dump = ( + singleNodeRoute + ? response + : Object.values(response)[0] + ) as Buffer; + + // restore without cleaning the lib and/or overwrite option causes an error + await expect( + client.functionRestore(dump, { route: route }), + ).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // APPEND policy also fails for the same reason (name collision) + await expect( + client.functionRestore(dump, { + policy: FunctionRestorePolicy.APPEND, + route: route, + }), + ).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // REPLACE policy succeeds + expect( + await client.functionRestore(dump, { + policy: FunctionRestorePolicy.REPLACE, + route: route, + }), + ).toEqual("OK"); + // but nothing changed - all code overwritten + expect( + await client.functionList( + { withCode: true }, + route, + ), + ).toEqual(flist); + + // create lib with another name, but with the same function names + expect( + await client.functionFlush( + FlushMode.SYNC, + route, + ), + ).toEqual("OK"); + code = generateLuaLibCode( + name2, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(name2); + + // REPLACE policy now fails due to a name collision + await expect( + client.functionRestore(dump, { route: route }), + ).rejects.toThrow( + new RegExp( + `Function ${name1}|${name2} already exists`, + ), + ); + + // FLUSH policy succeeds, but deletes the second lib + expect( + await client.functionRestore(dump, { + policy: FunctionRestorePolicy.FLUSH, + route: route, + }), + ).toEqual("OK"); + expect( + await client.functionList( + { withCode: true }, + route, + ), + ).toEqual(flist); + + // call restored functions + let res = await client.fcallWithRoute( + name1, + ["meow", "woem"], + route, + ); + + if (singleNodeRoute) { + expect(res).toEqual("meow"); + } else { + Object.values( + res as Record, + ).forEach((r) => expect(r).toEqual("meow")); + } + + res = await client.fcallWithRoute( + name2, + ["meow", "woem"], + route, + ); + + if (singleNodeRoute) { + expect(res).toEqual(2); + } else { + Object.values( + res as Record, + ).forEach((r) => expect(r).toEqual(2)); + } + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }); }, ); it( @@ -1272,7 +1440,7 @@ describe("GlideClusterClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClusterClient.createClient(config); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 4428f6018b..6bd44bcadc 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BaseClientConfiguration, BitFieldGet, BitFieldIncrBy, BitFieldOverflow, @@ -56,16 +57,18 @@ import { export type BaseClient = GlideClient | GlideClusterClient; -export function runBaseTests(config: { +// Same as `BaseClientConfiguration`, but all fields are optional +export type ClientConfig = Partial; + +export function runBaseTests(config: { init: ( protocol: ProtocolVersion, - clientName?: string, + configOverrides?: ClientConfig, ) => Promise<{ - context: Context; client: BaseClient; cluster: RedisCluster; }>; - close: (context: Context, testSucceeded: boolean) => void; + close: (testSucceeded: boolean) => void; timeout?: number; }) { runCommonTests({ @@ -77,11 +80,11 @@ export function runBaseTests(config: { const runTest = async ( test: (client: BaseClient, cluster: RedisCluster) => Promise, protocol: ProtocolVersion, - clientName?: string, + configOverrides?: ClientConfig, ) => { - const { context, client, cluster } = await config.init( + const { client, cluster } = await config.init( protocol, - clientName, + configOverrides, ); let testSucceeded = false; @@ -89,7 +92,7 @@ export function runBaseTests(config: { await test(client, cluster); testSucceeded = true; } finally { - config.close(context, testSucceeded); + config.close(testSucceeded); } }; @@ -161,7 +164,7 @@ export function runBaseTests(config: { expect(await client.clientGetName()).toBe("TEST_CLIENT"); }, protocol, - "TEST_CLIENT", + { clientName: "TEST_CLIENT" }, ); }, config.timeout, @@ -9673,20 +9676,20 @@ export function runBaseTests(config: { ); } -export function runCommonTests(config: { - init: () => Promise<{ context: Context; client: Client }>; - close: (context: Context, testSucceeded: boolean) => void; +export function runCommonTests(config: { + init: () => Promise<{ client: Client }>; + close: (testSucceeded: boolean) => void; timeout?: number; }) { const runTest = async (test: (client: Client) => Promise) => { - const { context, client } = await config.init(); + const { client } = await config.init(); let testSucceeded = false; try { await test(client); testSucceeded = true; } finally { - config.close(context, testSucceeded); + config.close(testSucceeded); } }; diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a987b285a5..2ac2cfda7a 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -338,7 +338,7 @@ export async function testTeardown( export const getClientConfigurationOption = ( addresses: [string, number][], protocol: ProtocolVersion, - timeout?: number, + configOverrides?: Partial, ): BaseClientConfiguration => { return { addresses: addresses.map(([host, port]) => ({ @@ -346,7 +346,7 @@ export const getClientConfigurationOption = ( port, })), protocol, - ...(timeout && { requestTimeout: timeout }), + ...configOverrides, }; }; @@ -357,7 +357,9 @@ export async function flushAndCloseClient( ) { await testTeardown( cluster_mode, - getClientConfigurationOption(addresses, ProtocolVersion.RESP3, 2000), + getClientConfigurationOption(addresses, ProtocolVersion.RESP3, { + requestTimeout: 2000, + }), ); // some tests don't initialize a client From a96ac1da933f7d75c69771dd9ec0d7ac272c6634 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Mon, 19 Aug 2024 15:46:33 -0700 Subject: [PATCH 191/236] Added transaction supports for DUMP and RESTORE (#2159) * Added transaction supports for DUMP and RESTORE Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 4 +-- node/src/Transaction.ts | 41 ++++++++++++++++++++++++++++- node/tests/SharedTests.ts | 54 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8dba939e4..79c4a598eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added WAIT command ([#2113](https://github.com/valkey-io/valkey-glide/pull/2113)) * Node: Added DUMP and RESTORE commands ([#2126](https://github.com/valkey-io/valkey-glide/pull/2126)) +* Node: Added transaction supports for DUMP and RESTORE ([#2159](https://github.com/valkey-io/valkey-glide/pull/2159)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) * Node: Added ZRANGESTORE command ([#2068](https://github.com/valkey-io/valkey-glide/pull/2068)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8160c81f8d..b231738d76 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1108,7 +1108,7 @@ export class BaseClient { /** * Serialize the value stored at `key` in a Valkey-specific format and return it to the user. * - * @See {@link https://valkey.io/commands/dump/|valkey.io} for details. + * @see {@link https://valkey.io/commands/dump/|valkey.io} for details. * * @param key - The `key` to serialize. * @returns The serialized value of the data stored at `key`. If `key` does not exist, `null` will be returned. @@ -1135,7 +1135,7 @@ export class BaseClient { * Create a `key` associated with a `value` that is obtained by deserializing the provided * serialized `value` (obtained via {@link dump}). * - * @See {@link https://valkey.io/commands/restore/|valkey.io} for details. + * @see {@link https://valkey.io/commands/restore/|valkey.io} for details. * @remarks `options.idletime` and `options.frequency` modifiers cannot be set at the same time. * * @param key - The `key` to create. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index dc168f95b1..90f719bae2 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,7 +4,6 @@ import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars - Decoder, // eslint-disable-line @typescript-eslint/no-unused-vars GlideString, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; @@ -47,6 +46,7 @@ import { RangeByIndex, RangeByLex, RangeByScore, + RestoreOptions, ReturnTypeXinfoStream, // eslint-disable-line @typescript-eslint/no-unused-vars ScoreFilter, SearchOrigin, @@ -86,6 +86,7 @@ import { createDecr, createDecrBy, createDel, + createDump, createEcho, createExists, createExpire, @@ -175,6 +176,7 @@ import { createRandomKey, createRename, createRenameNX, + createRestore, createSAdd, createSCard, createSDiff, @@ -417,6 +419,43 @@ export class BaseTransaction> { return this.addAndReturn(createDel(keys)); } + /** + * Serialize the value stored at `key` in a Valkey-specific format and return it to the user. + * + * @see {@link https://valkey.io/commands/dump/|valkey.io} for details. + * @remarks To execute a transaction with a `dump` command, the `exec` command requires `Decoder.Bytes` to handle the response. + * + * @param key - The `key` to serialize. + * + * Command Response - The serialized value of the data stored at `key`. If `key` does not exist, `null` will be returned. + */ + public dump(key: GlideString): T { + return this.addAndReturn(createDump(key)); + } + + /** + * Create a `key` associated with a `value` that is obtained by deserializing the provided + * serialized `value` (obtained via {@link dump}). + * + * @see {@link https://valkey.io/commands/restore/|valkey.io} for details. + * @remarks `options.idletime` and `options.frequency` modifiers cannot be set at the same time. + * + * @param key - The `key` to create. + * @param ttl - The expiry time (in milliseconds). If `0`, the `key` will persist. + * @param value - The serialized value to deserialize and assign to `key`. + * @param options - (Optional) Restore options {@link RestoreOptions}. + * + * Command Response - Return "OK" if the `key` was successfully restored with a `value`. + */ + public restore( + key: GlideString, + ttl: number, + value: Buffer, + options?: RestoreOptions, + ): T { + return this.addAndReturn(createRestore(key, ttl, value, options)); + } + /** Get the name of the connection on which the transaction is being executed. * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 6bd44bcadc..23c65ba5ae 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6244,6 +6244,8 @@ export function runBaseTests(config: { const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); const key3 = "{key}-3" + uuidv4(); + const key4 = "{key}-4" + uuidv4(); + const key5 = "{key}-5" + uuidv4(); const nonExistingkey = "{nonExistingkey}-" + uuidv4(); const value = "orange"; const valueEncode = Buffer.from(value); @@ -6254,7 +6256,7 @@ export function runBaseTests(config: { expect(await client.dump(nonExistingkey)).toBeNull(); // Dump existing key - const data = (await client.dump(key1)) as Buffer; + let data = (await client.dump(key1)) as Buffer; expect(data).not.toBeNull(); // Restore to a new key without option @@ -6342,6 +6344,56 @@ export function runBaseTests(config: { await expect( client.restore(key2, 0, valueEncode, { replace: true }), ).rejects.toThrow("DUMP payload version or checksum are wrong"); + + // Transaction tests + let response = + client instanceof GlideClient + ? await client.exec( + new Transaction().dump(key1), + Decoder.Bytes, + ) + : await client.exec( + new ClusterTransaction().dump(key1), + { decoder: Decoder.Bytes }, + ); + expect(response?.[0]).not.toBeNull(); + data = response?.[0] as Buffer; + + // Restore with `String` exec decoder + response = + client instanceof GlideClient + ? await client.exec( + new Transaction() + .restore(key4, 0, data) + .get(key4), + Decoder.String, + ) + : await client.exec( + new ClusterTransaction() + .restore(key4, 0, data) + .get(key4), + { decoder: Decoder.String }, + ); + expect(response?.[0]).toEqual("OK"); + expect(response?.[1]).toEqual(value); + + // Restore with `Bytes` exec decoder + response = + client instanceof GlideClient + ? await client.exec( + new Transaction() + .restore(key5, 0, data) + .get(key5), + Decoder.Bytes, + ) + : await client.exec( + new ClusterTransaction() + .restore(key5, 0, data) + .get(key5), + { decoder: Decoder.Bytes }, + ); + expect(response?.[0]).toEqual("OK"); + expect(response?.[1]).toEqual(valueEncode); }, protocol); }, config.timeout, From d91fbabd206683f7c541ab5cab79b41976c4db36 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:56:05 -0700 Subject: [PATCH 192/236] Node: add ZINTER and ZUNION commands (#2146) * Node: add ZINTER and ZUNION commands Signed-off-by: TJ Zhang * update tsdoc Signed-off-by: TJ Zhang * address commentes Signed-off-by: TJ Zhang * Address PR comments Signed-off-by: Jonathan Louie * Fix ESLint issues Signed-off-by: Jonathan Louie * Use createZCmdArgs instead of createZCmdStoreArgs for createZUnionStore Signed-off-by: Jonathan Louie * Fix Prettier suggestions Signed-off-by: Jonathan Louie --------- Signed-off-by: TJ Zhang Signed-off-by: Jonathan Louie Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Co-authored-by: TJ Zhang Co-authored-by: Jonathan Louie Co-authored-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 137 ++++++++- node/src/Commands.ts | 59 +++- node/src/Transaction.ts | 90 +++++- node/tests/GlideClusterClient.test.ts | 4 + node/tests/SharedTests.ts | 412 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 24 ++ 7 files changed, 717 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c4a598eb..817b975922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ * Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) * Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) +* Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b231738d76..450eb9f774 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -196,6 +196,7 @@ import { createZDiffStore, createZDiffWithScores, createZIncrBy, + createZInter, createZInterCard, createZInterstore, createZLexCount, @@ -216,6 +217,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnion, createZUnionStore, } from "./Commands"; import { @@ -3808,7 +3810,7 @@ export class BaseClient { /** * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. - * To get the result directly, see `zinter_withscores`. + * To get the result directly, see {@link zinterWithScores}. * * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. @@ -3817,7 +3819,8 @@ export class BaseClient { * @param keys - The keys of the sorted sets with possible formats: * string[] - for keys only. * KeyWeight[] - for weighted keys with score multipliers. - * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * @returns The number of elements in the resulting sorted set stored at `destination`. * * @example @@ -3826,9 +3829,9 @@ export class BaseClient { * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) * await client.zadd("key2", {"member1": 9.5}) * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. - * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. - * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. * ``` */ public async zinterstore( @@ -3841,6 +3844,132 @@ export class BaseClient { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + * To get the scores as well, see {@link zinterWithScores}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * @returns The resulting array of intersecting elements. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result = await client.zinter(["key1", "key2"]); + * console.log(result); // Output: ['member1'] + * ``` + */ + public zinter(keys: string[]): Promise { + return this.createWritePromise(createZInter(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements with scores. + * To get the elements only, see {@link zinter}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * @returns The resulting sorted set with scores. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result1 = await client.zinterWithScores(["key1", "key2"]); + * console.log(result1); // Output: {'member1': 20} - "member1" with score of 20 is the result + * const result2 = await client.zinterWithScores(["key1", "key2"], AggregationType.MAX) + * console.log(result2); // Output: {'member1': 10.5} - "member1" with score of 10.5 is the result. + * ``` + */ + public zinterWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise> { + return this.createWritePromise( + createZInter(keys, aggregationType, true), + ); + } + + /** + * Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + * To get the scores as well, see {@link zunionWithScores}. + * + * To store the result in a key as a sorted set, see {@link zunionStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * @returns The resulting array of union elements. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result = await client.zunion(["key1", "key2"]); + * console.log(result); // Output: ['member1', 'member2'] + * ``` + */ + public zunion(keys: string[]): Promise { + return this.createWritePromise(createZUnion(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of union elements with scores. + * To get the elements only, see {@link zunion}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * @returns The resulting sorted set with scores. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result1 = await client.zunionWithScores(["key1", "key2"]); + * console.log(result1); // {'member1': 20, 'member2': 8.2} + * const result2 = await client.zunionWithScores(["key1", "key2"], "MAX"); + * console.log(result2); // {'member1': 10.5, 'member2': 8.2} + * ``` + */ + public zunionWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise> { + return this.createWritePromise( + createZUnion(keys, aggregationType, true), + ); + } + /** * Returns a random member from the sorted set stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f5e5ac55a8..ba07f24a41 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1431,16 +1431,59 @@ export function createZInterstore( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): command_request.Command { - const args = createZCmdStoreArgs(destination, keys, aggregationType); + const args = createZCmdArgs(keys, { + aggregationType, + withScores: false, + destination, + }); return createCommand(RequestType.ZInterStore, args); } -function createZCmdStoreArgs( - destination: string, +/** + * @internal + */ +export function createZInter( keys: string[] | KeyWeight[], aggregationType?: AggregationType, + withScores?: boolean, +): command_request.Command { + const args = createZCmdArgs(keys, { aggregationType, withScores }); + return createCommand(RequestType.ZInter, args); +} + +/** + * @internal + */ +export function createZUnion( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + withScores?: boolean, +): command_request.Command { + const args = createZCmdArgs(keys, { aggregationType, withScores }); + return createCommand(RequestType.ZUnion, args); +} + +/** + * @internal + * Helper function for Zcommands (ZInter, ZinterStore, ZUnion..) that arranges arguments in the server's required order. + */ +function createZCmdArgs( + keys: string[] | KeyWeight[], + options: { + aggregationType?: AggregationType; + withScores?: boolean; + destination?: string; + }, ): string[] { - const args: string[] = [destination, keys.length.toString()]; + const args = []; + + const destination = options.destination; + + if (destination) { + args.push(destination); + } + + args.push(keys.length.toString()); if (typeof keys[0] === "string") { args.push(...(keys as string[])); @@ -1451,10 +1494,16 @@ function createZCmdStoreArgs( args.push("WEIGHTS", ...weights); } + const aggregationType = options.aggregationType; + if (aggregationType) { args.push("AGGREGATE", aggregationType); } + if (options.withScores) { + args.push("WITHSCORES"); + } + return args; } @@ -1540,7 +1589,7 @@ export function createZUnionStore( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): command_request.Command { - const args = createZCmdStoreArgs(destination, keys, aggregationType); + const args = createZCmdArgs(keys, { destination, aggregationType }); return createCommand(RequestType.ZUnionStore, args); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 90f719bae2..df89635de6 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -232,6 +232,7 @@ import { createZDiffStore, createZDiffWithScores, createZIncrBy, + createZInter, createZInterCard, createZInterstore, createZLexCount, @@ -252,6 +253,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnion, createZUnionStore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1954,11 +1956,15 @@ export class BaseTransaction> { * * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for details. * + * @remarks Since Valkey version 6.2.0. + * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: * string[] - for keys only. * KeyWeight[] - for weighted keys with score multipliers. - * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * * Command Response - The number of elements in the resulting sorted set stored at `destination`. */ public zinterstore( @@ -1971,6 +1977,88 @@ export class BaseTransaction> { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + * To get the scores as well, see {@link zinterWithScores}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - The resulting array of intersecting elements. + */ + public zinter(keys: string[]): T { + return this.addAndReturn(createZInter(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements with scores. + * To get the elements only, see {@link zinter}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * + * Command Response - The resulting sorted set with scores. + */ + public zinterWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn(createZInter(keys, aggregationType, true)); + } + + /** + * Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + * To get the scores as well, see {@link zunionWithScores}. + * + * To store the result in a key as a sorted set, see {@link zunionStore}. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - The resulting array of union elements. + */ + public zunion(keys: string[]): T { + return this.addAndReturn(createZUnion(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of union elements with scores. + * To get the elements only, see {@link zunion}. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * + * Command Response - The resulting sorted set with scores. + */ + public zunionWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn(createZUnion(keys, aggregationType, true)); + } + /** * Returns a random member from the sorted set stored at `key`. * diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 65d39b1703..a4669086b5 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -406,6 +406,10 @@ describe("GlideClusterClient", () => { { radius: 5, unit: GeoUnit.METERS }, ), client.zrangeStore("abc", "zyx", { start: 0, stop: -1 }), + client.zinter(["abc", "zxy", "lkn"]), + client.zinterWithScores(["abc", "zxy", "lkn"]), + client.zunion(["abc", "zxy", "lkn"]), + client.zunionWithScores(["abc", "zxy", "lkn"]), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 23c65ba5ae..0443ccb837 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4768,6 +4768,418 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZinter = await client.zinter([key1, key2]); + const expectedZinter = ["one", "two"]; + expect(resultZinter).toEqual(expectedZinter); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZinterWithScores = await client.zinterWithScores([ + key1, + key2, + ]); + const expectedZinterWithScores = { + one: 2.5, + two: 4.5, + }; + expect(resultZinterWithScores).toEqual( + expectedZinterWithScores, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with max aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MAX score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "MAX", + ); + const expectedMapMax = { + one: 1.5, + two: 2.5, + }; + expect(zinterWithScoresResults).toEqual(expectedMapMax); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with min aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MIN score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "MIN", + ); + const expectedMapMin = { + one: 1.0, + two: 2.0, + }; + expect(zinterWithScoresResults).toEqual(expectedMapMin); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with sum aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the SUM score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "SUM", + ); + const expectedMapSum = { + one: 2.5, + two: 4.5, + }; + expect(zinterWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with weights and aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the SUM score of elements with weights + const zinterWithScoresResults = await client.zinterWithScores( + [ + [key1, 3], + [key2, 2], + ], + "SUM", + ); + const expectedMapSum = { + one: 6, + two: 11, + }; + expect(zinterWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter empty test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + + // Non existing key zinter + expect( + await client.zinter([key1, "{testKey}-non_existing_key"]), + ).toEqual([]); + + // Non existing key zinterWithScores + expect( + await client.zinterWithScores([ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual({}); + + // Empty list check zinter + await expect(client.zinter([])).rejects.toThrow(); + + // Empty list check zinterWithScores + await expect(client.zinterWithScores([])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZunion = await client.zunion([key1, key2]); + const expectedZunion = ["one", "two", "three"]; + + expect(resultZunion.sort()).toEqual(expectedZunion.sort()); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZunionWithScores = await client.zunionWithScores([ + key1, + key2, + ]); + const expectedZunionWithScores = { + one: 2.5, + two: 4.5, + three: 3.5, + }; + expect(resultZunionWithScores).toEqual( + expectedZunionWithScores, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with max aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MAX score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "MAX", + ); + const expectedMapMax = { + one: 1.5, + two: 2.5, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapMax); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with min aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MIN score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "MIN", + ); + const expectedMapMin = { + one: 1.0, + two: 2.0, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapMin); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with sum aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the SUM score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "SUM", + ); + const expectedMapSum = { + one: 2.5, + two: 4.5, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with weights and aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the SUM score of elements with weights + const zunionWithScoresResults = await client.zunionWithScores( + [ + [key1, 3], + [key2, 2], + ], + "SUM", + ); + const expectedMapSum = { + one: 6, + two: 11, + three: 7, + }; + expect(zunionWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion empty test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + + // Non existing key zunion + expect( + await client.zunion([key1, "{testKey}-non_existing_key"]), + ).toEqual(["one", "two"]); + + // Non existing key zunionWithScores + expect( + await client.zunionWithScores([ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(membersScores1); + + // Empty list check zunion + await expect(client.zunion([])).rejects.toThrow(); + + // Empty list check zunionWithScores + await expect(client.zunionWithScores([])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `type test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2ac2cfda7a..1e6ada1a34 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -636,6 +636,8 @@ export async function transactionTest( const key23 = "{key}" + uuidv4(); // zset random const key24 = "{key}" + uuidv4(); // list value const key25 = "{key}" + uuidv4(); // Geospatial Data/ZSET + const key26 = "{key}" + uuidv4(); // sorted set + const key27 = "{key}" + uuidv4(); // sorted set const field = uuidv4(); const value = uuidv4(); const groupName1 = uuidv4(); @@ -996,6 +998,28 @@ export async function transactionTest( responseData.push(['zmscore(key12, ["two", "one"]', [2.0, 1.0]]); baseTransaction.zinterstore(key12, [key12, key13]); responseData.push(["zinterstore(key12, [key12, key13])", 0]); + + if (gte(version, "6.2.0")) { + baseTransaction.zadd(key26, { one: 1, two: 2 }); + responseData.push(["zadd(key26, { one: 1, two: 2 })", 2]); + baseTransaction.zadd(key27, { one: 1, two: 2, three: 3.5 }); + responseData.push([ + "zadd(key27, { one: 1, two: 2, three: 3.5 })", + 3, + ]); + baseTransaction.zinter([key27, key26]); + responseData.push(["zinter([key27, key26])", ["one", "two"]]); + baseTransaction.zinterWithScores([key27, key26]); + responseData.push([ + "zinterWithScores([key27, key26])", + { one: 2, two: 4 }, + ]); + baseTransaction.zunionWithScores([key27, key26]); + responseData.push([ + "zunionWithScores([key27, key26])", + { one: 2, two: 4, three: 3.5 }, + ]); + } } else { baseTransaction.zinterstore(key12, [key12, key13]); responseData.push(["zinterstore(key12, [key12, key13])", 2]); From 45001ed01e976dd859e1d8cc253f0e2c0d1d4a4d Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:43:27 +0300 Subject: [PATCH 193/236] Bump redis-rs + Route Function Stats to all nodes (#2117) --------- Signed-off-by: Shoham Elias Signed-off-by: Yury-Fridlyand Signed-off-by: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Co-authored-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 3 +- .../src/client/reconnecting_connection.rs | 2 +- glide-core/src/client/standalone_client.rs | 17 ++- .../src/main/java/glide/api/BaseClient.java | 35 ++++- .../src/main/java/glide/api/GlideClient.java | 9 +- .../java/glide/api/GlideClusterClient.java | 30 ----- .../ScriptingAndFunctionsClusterCommands.java | 2 +- .../ScriptingAndFunctionsCommands.java | 73 ++++++----- .../test/java/glide/api/GlideClientTest.java | 24 ++-- .../java/glide/standalone/CommandTests.java | 24 +++- node/npm/glide/index.ts | 6 +- node/src/Commands.ts | 20 ++- node/src/GlideClient.ts | 61 +++++---- node/src/GlideClusterClient.ts | 12 +- node/src/Transaction.ts | 4 +- node/tests/GlideClient.test.ts | 16 ++- node/tests/GlideClusterClient.test.ts | 8 +- node/tests/TestUtilities.ts | 4 +- .../glide/async_commands/cluster_commands.py | 14 +- .../async_commands/standalone_commands.py | 42 ++++-- .../glide/async_commands/transaction.py | 2 +- python/python/glide/constants.py | 14 +- python/python/tests/test_async_client.py | 121 ++++++++++-------- python/python/tests/utils/utils.py | 6 +- submodules/redis-rs | 2 +- 25 files changed, 328 insertions(+), 223 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 817b975922..af4e2672e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,13 +84,14 @@ * Node: Added PUBSUB * commands ([#2090](https://github.com/valkey-io/valkey-glide/pull/2090)) * Python: Added PUBSUB * commands ([#2043](https://github.com/valkey-io/valkey-glide/pull/2043)) * Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) -* Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) +* Node: Added BZPOPMAX & BZPOPMIN command ([#2077](https://github.com/valkey-io/valkey-glide/pull/2077)) * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) * Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) * Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) +* Core: Change FUNCTION STATS command to return multi node response for standalone mode ([#2117](https://github.com/valkey-io/valkey-glide/pull/2117)) #### Fixes * Java: Add overloads for XADD to allow duplicate entry keys ([#1970](https://github.com/valkey-io/valkey-glide/pull/1970)) diff --git a/glide-core/src/client/reconnecting_connection.rs b/glide-core/src/client/reconnecting_connection.rs index c76da9cf42..4d962e40dd 100644 --- a/glide-core/src/client/reconnecting_connection.rs +++ b/glide-core/src/client/reconnecting_connection.rs @@ -165,7 +165,7 @@ impl ReconnectingConnection { create_connection(backend, connection_retry_strategy, push_sender).await } - fn node_address(&self) -> String { + pub(crate) fn node_address(&self) -> String { self.inner .backend .connection_info diff --git a/glide-core/src/client/standalone_client.rs b/glide-core/src/client/standalone_client.rs index a8350651e9..fd17c538e8 100644 --- a/glide-core/src/client/standalone_client.rs +++ b/glide-core/src/client/standalone_client.rs @@ -312,7 +312,22 @@ impl StandaloneClient { Some(ResponsePolicy::CombineMaps) => future::try_join_all(requests) .await .and_then(cluster_routing::combine_map_results), - Some(ResponsePolicy::Special) | None => { + Some(ResponsePolicy::Special) => { + // Await all futures and collect results + let results = future::try_join_all(requests).await?; + // Create key-value pairs where the key is the node address and the value is the corresponding result + let node_result_pairs = self + .inner + .nodes + .iter() + .zip(results) + .map(|(node, result)| (Value::BulkString(node.node_address().into()), result)) + .collect(); + + Ok(Value::Map(node_result_pairs)) + } + + None => { // This is our assumption - if there's no coherent way to aggregate the responses, we just collect them in an array, and pass it to the user. // TODO - once Value::Error is merged, we can use join_all and report separate errors and also pass successes. future::try_join_all(requests).await.map(Value::Array) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 91182e797e..0638201f9e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -208,6 +208,7 @@ import glide.api.commands.StreamBaseCommands; import glide.api.commands.StringBaseCommands; import glide.api.commands.TransactionsBaseCommands; +import glide.api.models.ClusterValue; import glide.api.models.GlideString; import glide.api.models.PubSubMessage; import glide.api.models.Script; @@ -696,7 +697,7 @@ protected Map[] handleFunctionListResponseBinary(Object[] r return data; } - /** Process a FUNCTION STATS standalone response. */ + /** Process a FUNCTION STATS response from one node. */ protected Map> handleFunctionStatsResponse( Map> response) { Map runningScriptInfo = response.get("running_script"); @@ -707,7 +708,7 @@ protected Map> handleFunctionStatsResponse( return response; } - /** Process a FUNCTION STATS standalone response. */ + /** Process a FUNCTION STATS response from one node. */ protected Map> handleFunctionStatsBinaryResponse( Map> response) { Map runningScriptInfo = response.get(gs("running_script")); @@ -718,6 +719,36 @@ protected Map> handleFunctionStatsBinaryRe return response; } + /** Process a FUNCTION STATS cluster response. */ + protected ClusterValue>> handleFunctionStatsResponse( + Response response, boolean isSingleValue) { + if (isSingleValue) { + return ClusterValue.ofSingleValue(handleFunctionStatsResponse(handleMapResponse(response))); + } else { + Map>> data = handleMapResponse(response); + for (var nodeInfo : data.entrySet()) { + nodeInfo.setValue(handleFunctionStatsResponse(nodeInfo.getValue())); + } + return ClusterValue.ofMultiValue(data); + } + } + + /** Process a FUNCTION STATS cluster response. */ + protected ClusterValue>> + handleFunctionStatsBinaryResponse(Response response, boolean isSingleValue) { + if (isSingleValue) { + return ClusterValue.ofSingleValue( + handleFunctionStatsBinaryResponse(handleBinaryStringMapResponse(response))); + } else { + Map>> data = + handleBinaryStringMapResponse(response); + for (var nodeInfo : data.entrySet()) { + nodeInfo.setValue(handleFunctionStatsBinaryResponse(nodeInfo.getValue())); + } + return ClusterValue.ofMultiValueBinary(data); + } + } + /** Process a LCS key1 key2 IDX response */ protected Map handleLcsIdxResponse(Map response) throws GlideException { diff --git a/java/client/src/main/java/glide/api/GlideClient.java b/java/client/src/main/java/glide/api/GlideClient.java index ee85b4b393..32c5a5cc28 100644 --- a/java/client/src/main/java/glide/api/GlideClient.java +++ b/java/client/src/main/java/glide/api/GlideClient.java @@ -444,19 +444,20 @@ public CompletableFuture functionKill() { } @Override - public CompletableFuture>> functionStats() { + public CompletableFuture>>> functionStats() { return commandManager.submitNewCommand( FunctionStats, new String[0], - response -> handleFunctionStatsResponse(handleMapResponse(response))); + response -> handleFunctionStatsResponse(response, false).getMultiValue()); } @Override - public CompletableFuture>> functionStatsBinary() { + public CompletableFuture>>> + functionStatsBinary() { return commandManager.submitNewCommand( FunctionStats, new GlideString[0], - response -> handleFunctionStatsBinaryResponse(handleBinaryStringMapResponse(response))); + response -> handleFunctionStatsBinaryResponse(response, false).getMultiValue()); } @Override diff --git a/java/client/src/main/java/glide/api/GlideClusterClient.java b/java/client/src/main/java/glide/api/GlideClusterClient.java index bd01bce6b1..2eb52c10ef 100644 --- a/java/client/src/main/java/glide/api/GlideClusterClient.java +++ b/java/client/src/main/java/glide/api/GlideClusterClient.java @@ -946,36 +946,6 @@ public CompletableFuture functionKill(@NonNull Route route) { FunctionKill, new String[0], route, this::handleStringResponse); } - /** Process a FUNCTION STATS cluster response. */ - protected ClusterValue>> handleFunctionStatsResponse( - Response response, boolean isSingleValue) { - if (isSingleValue) { - return ClusterValue.ofSingleValue(handleFunctionStatsResponse(handleMapResponse(response))); - } else { - Map>> data = handleMapResponse(response); - for (var nodeInfo : data.entrySet()) { - nodeInfo.setValue(handleFunctionStatsResponse(nodeInfo.getValue())); - } - return ClusterValue.ofMultiValue(data); - } - } - - /** Process a FUNCTION STATS cluster response. */ - protected ClusterValue>> - handleFunctionStatsBinaryResponse(Response response, boolean isSingleValue) { - if (isSingleValue) { - return ClusterValue.ofSingleValue( - handleFunctionStatsBinaryResponse(handleBinaryStringMapResponse(response))); - } else { - Map>> data = - handleBinaryStringMapResponse(response); - for (var nodeInfo : data.entrySet()) { - nodeInfo.setValue(handleFunctionStatsBinaryResponse(nodeInfo.getValue())); - } - return ClusterValue.ofMultiValueBinary(data); - } - } - @Override public CompletableFuture>>> functionStats() { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index 885fc16b81..0c4a9a891f 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -865,7 +865,7 @@ CompletableFuture> fcallReadOnly( /** * Kills a function that is currently executing.
* FUNCTION KILL terminates read-only functions only.
- * The command will be routed to all primary nodes. + * The command will be routed to all nodes. * * @since Valkey 7.0 and above. * @see valkey.io for details. diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index f6ca4890bb..7cb5bb3f36 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -328,7 +328,8 @@ CompletableFuture[]> functionListBinary( /** * Kills a function that is currently executing.
- * FUNCTION KILL terminates read-only functions only. + * FUNCTION KILL terminates read-only functions only. FUNCTION KILL runs + * on all nodes of the server, including primary and replicas. * * @since Valkey 7.0 and above. * @see valkey.io for details. @@ -343,11 +344,14 @@ CompletableFuture[]> functionListBinary( /** * Returns information about the function that's currently running and information about the - * available execution engines. + * available execution engines.
+ * FUNCTION STATS runs on all nodes of the server, including primary and replicas. + * The response includes a mapping from node address to the command response for that node. * * @since Valkey 7.0 and above. * @see valkey.io for details. - * @return A Map with two keys: + * @return A Map from node address to the command response for that node, where the + * command contains a Map with two keys: *
    *
  • running_script with information about the running script. *
  • engines with information about available engines and their stats. @@ -355,30 +359,35 @@ CompletableFuture[]> functionListBinary( * See example for more details. * @example *
    {@code
    -     * Map> response = client.functionStats().get();
    -     * Map runningScriptInfo = response.get("running_script");
    -     * if (runningScriptInfo != null) {
    -     *   String[] commandLine = (String[]) runningScriptInfo.get("command");
    -     *   System.out.printf("Server is currently running function '%s' with command line '%s', which has been running for %d ms%n",
    -     *       runningScriptInfo.get("name"), String.join(" ", commandLine), (long)runningScriptInfo.get("duration_ms"));
    -     * }
    -     * Map enginesInfo = response.get("engines");
    -     * for (String engineName : enginesInfo.keySet()) {
    -     *   Map engine = (Map) enginesInfo.get(engineName);
    -     *   System.out.printf("Server supports engine '%s', which has %d libraries and %d functions in total%n",
    -     *       engineName, engine.get("libraries_count"), engine.get("functions_count"));
    +     * Map>> response = client.functionStats().get();
    +     * for (String node : response.keySet()) {
    +     *   Map runningScriptInfo = response.get(node).get("running_script");
    +     *   if (runningScriptInfo != null) {
    +     *     String[] commandLine = (String[]) runningScriptInfo.get("command");
    +     *     System.out.printf("Node '%s' is currently running function '%s' with command line '%s', which has been running for %d ms%n",
    +     *         node, runningScriptInfo.get("name"), String.join(" ", commandLine), (long)runningScriptInfo.get("duration_ms"));
    +     *   }
    +     *   Map enginesInfo = response.get(node).get("engines");
    +     *   for (String engineName : enginesInfo.keySet()) {
    +     *     Map engine = (Map) enginesInfo.get(engineName);
    +     *     System.out.printf("Node '%s' supports engine '%s', which has %d libraries and %d functions in total%n",
    +     *         node, engineName, engine.get("libraries_count"), engine.get("functions_count"));
    +     *   }
          * }
          * }
    */ - CompletableFuture>> functionStats(); + CompletableFuture>>> functionStats(); /** * Returns information about the function that's currently running and information about the - * available execution engines. + * available execution engines.
    + * FUNCTION STATS runs on all nodes of the server, including primary and replicas. + * The response includes a mapping from node address to the command response for that node. * * @since Valkey 7.0 and above. * @see valkey.io for details. - * @return A Map with two keys: + * @return A Map from node address to the command response for that node, where the + * command contains a Map with two keys: *
      *
    • running_script with information about the running script. *
    • engines with information about available engines and their stats. @@ -386,20 +395,22 @@ CompletableFuture[]> functionListBinary( * See example for more details. * @example *
      {@code
      -     * Map> response = client.functionStats().get();
      -     * Map runningScriptInfo = response.get(gs("running_script"));
      -     * if (runningScriptInfo != null) {
      -     *   GlideString[] commandLine = (GlideString[]) runningScriptInfo.get(gs("command"));
      -     *   System.out.printf("Server is currently running function '%s' with command line '%s', which has been running for %d ms%n",
      -     *       runningScriptInfo.get(gs("name")), String.join(" ", Arrays.toString(commandLine)), (long)runningScriptInfo.get(gs("duration_ms")));
      -     * }
      -     * Map enginesInfo = response.get(gs("engines"));
      -     * for (GlideString engineName : enginesInfo.keySet()) {
      -     *   Map engine = (Map) enginesInfo.get(gs(engineName));
      -     *   System.out.printf("Server supports engine '%s', which has %d libraries and %d functions in total%n",
      -     *       engineName, engine.get(gs("libraries_count")), engine.get(gs("functions_count")));
      +     * Map>> response = client.functionStats().get();
      +     * for (String node : response.keySet()) {
      +     *   Map runningScriptInfo = response.get(gs(node)).get(gs("running_script"));
      +     *   if (runningScriptInfo != null) {
      +     *     GlideString[] commandLine = (GlideString[]) runningScriptInfo.get(gs("command"));
      +     *     System.out.printf("Node '%s' is currently running function '%s' with command line '%s', which has been running for %d ms%n",
      +     *         node, runningScriptInfo.get(gs("name")), String.join(" ", Arrays.toString(commandLine)), (long)runningScriptInfo.get(gs("duration_ms")));
      +     *   }
      +     *   Map enginesInfo = response.get(gs(node)).get(gs("engines"));
      +     *   for (GlideString engineName : enginesInfo.keySet()) {
      +     *     Map engine = (Map) enginesInfo.get(gs(engineName));
      +     *     System.out.printf("Node '%s' supports engine '%s', which has %d libraries and %d functions in total%n",
      +     *         node, engineName, engine.get(gs("libraries_count")), engine.get(gs("functions_count")));
      +     *   }
            * }
            * }
      */ - CompletableFuture>> functionStatsBinary(); + CompletableFuture>>> functionStatsBinary(); } diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 50e812a511..2170218b36 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -11485,18 +11485,21 @@ public void functionKill_returns_success() { public void functionStats_returns_success() { // setup String[] args = new String[0]; - Map> value = Map.of("1", Map.of("2", 2)); - CompletableFuture>> testResponse = new CompletableFuture<>(); + Map>> value = + Map.of("::1", Map.of("1", Map.of("2", 2))); + CompletableFuture>>> testResponse = + new CompletableFuture<>(); testResponse.complete(value); // match on protobuf request - when(commandManager.>>submitNewCommand( + when(commandManager.>>>submitNewCommand( eq(FunctionStats), eq(args), any())) .thenReturn(testResponse); // exercise - CompletableFuture>> response = service.functionStats(); - Map> payload = response.get(); + CompletableFuture>>> response = + service.functionStats(); + Map>> payload = response.get(); // verify assertEquals(testResponse, response); @@ -11508,20 +11511,21 @@ public void functionStats_returns_success() { public void functionStatsBinary_returns_success() { // setup GlideString[] args = new GlideString[0]; - Map> value = Map.of(gs("1"), Map.of(gs("2"), 2)); - CompletableFuture>> testResponse = + Map>> value = + Map.of("::1", Map.of(gs("1"), Map.of(gs("2"), 2))); + CompletableFuture>>> testResponse = new CompletableFuture<>(); testResponse.complete(value); // match on protobuf request - when(commandManager.>>submitNewCommand( + when(commandManager.>>>submitNewCommand( eq(FunctionStats), eq(args), any())) .thenReturn(testResponse); // exercise - CompletableFuture>> response = + CompletableFuture>>> response = service.functionStatsBinary(); - Map> payload = response.get(); + Map>> payload = response.get(); // verify assertEquals(testResponse, response); diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 5cdac40b9c..eeb8f6660d 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -986,7 +986,9 @@ public void functionStats() { assertEquals(libName, regularClient.functionLoad(code, true).get()); var response = regularClient.functionStats().get(); - checkFunctionStatsResponse(response, new String[0], 1, 1); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 1, 1); + } code = generateLuaLibCode( @@ -996,12 +998,16 @@ public void functionStats() { assertEquals(libName + "_2", regularClient.functionLoad(code, true).get()); response = regularClient.functionStats().get(); - checkFunctionStatsResponse(response, new String[0], 2, 3); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 2, 3); + } assertEquals(OK, regularClient.functionFlush(SYNC).get()); response = regularClient.functionStats().get(); - checkFunctionStatsResponse(response, new String[0], 0, 0); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 0, 0); + } } @Test @@ -1019,7 +1025,9 @@ public void functionStatsBinary() { assertEquals(libName, regularClient.functionLoad(code, true).get()); var response = regularClient.functionStatsBinary().get(); - checkFunctionStatsBinaryResponse(response, new GlideString[0], 1, 1); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 1, 1); + } code = generateLuaLibCodeBinary( @@ -1033,12 +1041,16 @@ public void functionStatsBinary() { assertEquals(gs(libName.toString() + "_2"), regularClient.functionLoad(code, true).get()); response = regularClient.functionStatsBinary().get(); - checkFunctionStatsBinaryResponse(response, new GlideString[0], 2, 3); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 2, 3); + } assertEquals(OK, regularClient.functionFlush(SYNC).get()); response = regularClient.functionStatsBinary().get(); - checkFunctionStatsBinaryResponse(response, new GlideString[0], 0, 0); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 0, 0); + } } @Test diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 276ebfa0b4..733de07d6a 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -107,7 +107,8 @@ function initialize() { GlideString, FunctionListOptions, FunctionListResponse, - FunctionStatsResponse, + FunctionStatsSingleResponse, + FunctionStatsFullResponse, FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, @@ -205,7 +206,8 @@ function initialize() { GlideClientConfiguration, FunctionListOptions, FunctionListResponse, - FunctionStatsResponse, + FunctionStatsSingleResponse, + FunctionStatsFullResponse, FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ba07f24a41..80206db1af 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2324,12 +2324,24 @@ export function createFunctionList( return createCommand(RequestType.FunctionList, args); } -/** Type of the response of `FUNCTION STATS` command. */ -export type FunctionStatsResponse = Record< +/** Response for `FUNCTION STATS` command on a single node. + * The response is a map with 2 keys: + * 1. Information about the current running function/script (or null if none). + * 2. Details about the execution engines. + */ +export type FunctionStatsSingleResponse = Record< string, | null - | Record - | Record> + | Record // Running function/script information + | Record> // Execution engines information +>; + +/** Full response for `FUNCTION STATS` command across multiple nodes. + * It maps node addresses to the per-node response. + */ +export type FunctionStatsFullResponse = Record< + string, // Node address + FunctionStatsSingleResponse >; /** @internal */ diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index e5bfb9a1b4..d542a4057d 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -17,7 +17,7 @@ import { FunctionListOptions, FunctionListResponse, FunctionRestorePolicy, - FunctionStatsResponse, + FunctionStatsFullResponse, InfoOptions, LolwutOptions, SortOptions, @@ -597,55 +597,54 @@ export class GlideClient extends BaseClient { * Returns information about the function that's currently running and information about the * available execution engines. * + * FUNCTION STATS runs on all nodes of the server, including primary and replicas. + * The response includes a mapping from node address to the command response for that node. + * * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @returns A `Record` with two keys: - * - `"running_script"` with information about the running script. - * - `"engines"` with information about available engines and their stats. - * - see example for more details. - * + * @returns A Record where the key is the node address and the value is a Record with two keys: + * - `"running_script"`: Information about the running script, or `null` if no script is running. + * - `"engines"`: Information about available engines and their stats. + * - see example for more details. * @example * ```typescript * const response = await client.functionStats(); - * console.log(response); // Output: + * console.log(response); // Example output: * // { - * // "running_script": - * // { - * // "name": "deep_thought", - * // "command": ["fcall", "deep_thought", "0"], - * // "duration_ms": 5008 - * // }, - * // "engines": - * // { - * // "LUA": - * // { - * // "libraries_count": 2, - * // "functions_count": 3 + * // "127.0.0.1:6379": { // Response from the primary node + * // "running_script": { + * // "name": "foo", + * // "command": ["FCALL", "foo", "0", "hello"], + * // "duration_ms": 7758 + * // }, + * // "engines": { + * // "LUA": { + * // "libraries_count": 1, + * // "functions_count": 1 + * // } * // } - * // } - * // } - * // Output if no scripts running: - * // { - * // "running_script": null - * // "engines": - * // { - * // "LUA": - * // { - * // "libraries_count": 2, - * // "functions_count": 3 + * // }, + * // "127.0.0.1:6380": { // Response from a replica node + * // "running_script": null, + * // "engines": { + * // "LUA": { + * // "libraries_count": 1, + * // "functions_count": 1 + * // } * // } * // } * // } * ``` */ - public async functionStats(): Promise { + public async functionStats(): Promise { return this.createWritePromise(createFunctionStats()); } /** * Kills a function that is currently executing. * `FUNCTION KILL` terminates read-only functions only. + * `FUNCTION KILL` runs on all nodes of the server, including primary and replicas. * * @see {@link https://valkey.io/commands/function-kill/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index f4f36e51da..c9dc39f067 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -17,7 +17,7 @@ import { FunctionListOptions, FunctionListResponse, FunctionRestorePolicy, - FunctionStatsResponse, + FunctionStatsSingleResponse, InfoOptions, LolwutOptions, SortClusterOptions, @@ -914,12 +914,14 @@ export class GlideClusterClient extends BaseClient { /** * Returns information about the function that's currently running and information about the * available execution engines. + * + * * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param route - The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed to all primary nodes. + * @param route - The command will be routed automatically to all nodes, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. * @returns A `Record` with two keys: * - `"running_script"` with information about the running script. * - `"engines"` with information about available engines and their stats. @@ -961,7 +963,7 @@ export class GlideClusterClient extends BaseClient { */ public async functionStats( route?: Routes, - ): Promise> { + ): Promise> { return this.createWritePromise(createFunctionStats(), { route: toProtobufRoute(route), }); @@ -975,7 +977,7 @@ export class GlideClusterClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param route - (Optional) The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed to all primary nodes. + * If not defined, the command will be routed to all nodes. * @returns `OK` if function is terminated. Otherwise, throws an error. * * @example diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index df89635de6..8d5d633fc9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -27,7 +27,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, // eslint-disable-line @typescript-eslint/no-unused-vars - FunctionStatsResponse, // eslint-disable-line @typescript-eslint/no-unused-vars + FunctionStatsSingleResponse, // eslint-disable-line @typescript-eslint/no-unused-vars GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -3182,7 +3182,7 @@ export class BaseTransaction> { * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * Command Response - A `Record` of type {@link FunctionStatsResponse} with two keys: + * Command Response - A `Record` of type {@link FunctionStatsSingleResponse} with two keys: * * - `"running_script"` with information about the running script. * - `"engines"` with information about available engines and their stats. diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index f5526104bb..6c93134fae 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -675,7 +675,10 @@ describe("GlideClient", () => { ).toEqual("one"); let functionStats = await client.functionStats(); - checkFunctionStatsResponse(functionStats, [], 1, 1); + + for (const response of Object.values(functionStats)) { + checkFunctionStatsResponse(response, [], 1, 1); + } let functionList = await client.functionList({ libNamePattern: libName, @@ -736,7 +739,10 @@ describe("GlideClient", () => { ); functionStats = await client.functionStats(); - checkFunctionStatsResponse(functionStats, [], 1, 2); + + for (const response of Object.values(functionStats)) { + checkFunctionStatsResponse(response, [], 1, 2); + } expect( await client.fcall(func2Name, [], ["one", "two"]), @@ -747,7 +753,11 @@ describe("GlideClient", () => { } finally { expect(await client.functionFlush()).toEqual("OK"); const functionStats = await client.functionStats(); - checkFunctionStatsResponse(functionStats, [], 0, 0); + + for (const response of Object.values(functionStats)) { + checkFunctionStatsResponse(response, [], 0, 0); + } + client.close(); } }, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index a4669086b5..8cd18e943a 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -30,7 +30,7 @@ import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode, FunctionRestorePolicy, - FunctionStatsResponse, + FunctionStatsSingleResponse, GeoUnit, SortOrder, } from "../build-ts/src/Commands"; @@ -833,7 +833,7 @@ describe("GlideClusterClient", () => { singleNodeRoute, (value) => checkFunctionStatsResponse( - value as FunctionStatsResponse, + value as FunctionStatsSingleResponse, [], 0, 0, @@ -875,7 +875,7 @@ describe("GlideClusterClient", () => { singleNodeRoute, (value) => checkFunctionStatsResponse( - value as FunctionStatsResponse, + value as FunctionStatsSingleResponse, [], 1, 1, @@ -966,7 +966,7 @@ describe("GlideClusterClient", () => { singleNodeRoute, (value) => checkFunctionStatsResponse( - value as FunctionStatsResponse, + value as FunctionStatsSingleResponse, [], 1, 2, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 1e6ada1a34..4605f89aa9 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -19,7 +19,7 @@ import { ClusterTransaction, FlushMode, FunctionListResponse, - FunctionStatsResponse, + FunctionStatsSingleResponse, GeoUnit, GeospatialData, GlideClient, @@ -461,7 +461,7 @@ export function checkFunctionListResponse( * @param functionCount - Expected functions count. */ export function checkFunctionStatsResponse( - response: FunctionStatsResponse, + response: FunctionStatsSingleResponse, runningFunction: string[], libCount: number, functionCount: number, diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 4371d23125..f42e25d032 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -18,7 +18,7 @@ TClusterResponse, TEncodable, TFunctionListResponse, - TFunctionStatsResponse, + TFunctionStatsSingleNodeResponse, TResult, TSingleNodeRoute, ) @@ -493,7 +493,7 @@ async def function_kill(self, route: Optional[Route] = None) -> TOK: See https://valkey.io/commands/function-kill/ for more details. Args: - route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + route (Optional[Route]): The command will be routed to all nodes, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Returns: @@ -588,7 +588,7 @@ async def fcall_ro_route( async def function_stats( self, route: Optional[Route] = None - ) -> TClusterResponse[TFunctionStatsResponse]: + ) -> TClusterResponse[TFunctionStatsSingleNodeResponse]: """ Returns information about the function that's currently running and information about the available execution engines. @@ -596,11 +596,11 @@ async def function_stats( See https://valkey.io/commands/function-stats/ for more details Args: - route (Optional[Route]): Specifies the routing configuration for the command. The client - will route the command to the nodes defined by `route`. + route (Optional[Route]): The command will be routed automatically to all nodes, unless `route` is provided, in which + case the client will route the command to the nodes defined by `route`. Defaults to None. Returns: - TClusterResponse[TFunctionStatsResponse]: A `Mapping` with two keys: + TClusterResponse[TFunctionStatsSingleNodeResponse]: A `Mapping` with two keys: - `running_script` with information about the running script. - `engines` with information about available engines and their stats. See example for more details. @@ -624,7 +624,7 @@ async def function_stats( Since: Valkey version 7.0.0. """ return cast( - TClusterResponse[TFunctionStatsResponse], + TClusterResponse[TFunctionStatsSingleNodeResponse], await self._execute_command(RequestType.FunctionStats, [], route), ) diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index 37815e14c4..1659134984 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -18,7 +18,7 @@ TOK, TEncodable, TFunctionListResponse, - TFunctionStatsResponse, + TFunctionStatsFullResponse, TResult, ) from glide.protobuf.command_request_pb2 import RequestType @@ -374,6 +374,8 @@ async def function_kill(self) -> TOK: Kills a function that is currently executing. This command only terminates read-only functions. + FUNCTION KILL runs on all nodes of the server, including primary and replicas. + See https://valkey.io/commands/function-kill/ for more details. Returns: @@ -390,39 +392,51 @@ async def function_kill(self) -> TOK: await self._execute_command(RequestType.FunctionKill, []), ) - async def function_stats(self) -> TFunctionStatsResponse: + async def function_stats(self) -> TFunctionStatsFullResponse: """ Returns information about the function that's currently running and information about the available execution engines. + FUNCTION STATS runs on all nodes of the server, including primary and replicas. + The response includes a mapping from node address to the command response for that node. + See https://valkey.io/commands/function-stats/ for more details Returns: - TFunctionStatsResponse: A `Mapping` with two keys: + TFunctionStatsFullResponse: A Map where the key is the node address and the value is a Map of two keys: - `running_script` with information about the running script. - `engines` with information about available engines and their stats. See example for more details. Examples: >>> await client.function_stats() - { - 'running_script': { - 'name': 'foo', - 'command': ['FCALL', 'foo', '0', 'hello'], - 'duration_ms': 7758 + {b"addr": { # Response from the master node + b'running_script': { + b'name': b'foo', + b'command': [b'FCALL', b'foo', b'0', b'hello'], + b'duration_ms': 7758 }, - 'engines': { - 'LUA': { - 'libraries_count': 1, - 'functions_count': 1, + b'engines': { + b'LUA': { + b'libraries_count': 1, + b'functions_count': 1, + } + } + }, + b"addr2": { # Response from a replica + b'running_script': None, + b"engines": { + b'LUA': { + b'libraries_count': 1, + b'functions_count': 1, } } - } + }} Since: Valkey version 7.0.0. """ return cast( - TFunctionStatsResponse, + TFunctionStatsFullResponse, await self._execute_command(RequestType.FunctionStats, []), ) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 3a03351224..8aa0f0fa1d 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -2028,7 +2028,7 @@ def function_stats(self: TTransaction) -> TTransaction: See https://valkey.io/commands/function-stats/ for more details Command Response: - TFunctionStatsResponse: A `Mapping` with two keys: + TFunctionStatsSingleNodeResponse: A `Mapping` with two keys: - `running_script` with information about the running script. - `engines` with information about available engines and their stats. See example for more details. diff --git a/python/python/glide/constants.py b/python/python/glide/constants.py index 24c30d8de7..754aacf6fa 100644 --- a/python/python/glide/constants.py +++ b/python/python/glide/constants.py @@ -42,15 +42,25 @@ Union[bytes, List[Mapping[bytes, Union[bytes, Set[bytes]]]]], ] ] -TFunctionStatsResponse = Mapping[ +# Response for function stats command on a single node. +# The response holds a map with 2 keys: Current running function / script and information about it, and the engines and the information about it. +TFunctionStatsSingleNodeResponse = Mapping[ bytes, Union[ None, Mapping[ - bytes, Union[Mapping[bytes, Mapping[bytes, int]], bytes, int, List[bytes]] + bytes, + Union[Mapping[bytes, Mapping[bytes, int]], bytes, int, List[bytes]], ], ], ] +# Full response for function stats command across multiple nodes. +# It maps node address to the per-node response. +TFunctionStatsFullResponse = Mapping[ + bytes, + TFunctionStatsSingleNodeResponse, +] + TXInfoStreamResponse = Mapping[ bytes, Union[bytes, int, Mapping[bytes, Optional[List[List[bytes]]]]] diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index d46e490b70..ce8008856a 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -76,7 +76,7 @@ ProtocolVersion, ServerCredentials, ) -from glide.constants import OK, TEncodable, TFunctionStatsResponse, TResult +from glide.constants import OK, TEncodable, TFunctionStatsSingleNodeResponse, TResult from glide.exceptions import TimeoutError as GlideTimeoutError from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient from glide.routes import ( @@ -8206,14 +8206,14 @@ async def test_function_delete_with_routing( await glide_client.function_delete(lib_name) assert "Library not found" in str(e) - @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_function_stats(self, glide_client: GlideClient): + async def test_function_stats(self, glide_client: TGlideClient): min_version = "7.0.0" if await check_if_server_version_lt(glide_client, min_version): return pytest.mark.skip(reason=f"Redis version required >= {min_version}") - lib_name = "functionStats" + lib_name = "functionStats_without_route" func_name = lib_name assert await glide_client.function_flush(FlushMode.SYNC) == OK @@ -8222,7 +8222,10 @@ async def test_function_stats(self, glide_client: GlideClient): assert await glide_client.function_load(code, True) == lib_name.encode() response = await glide_client.function_stats() - check_function_stats_response(response, [], 1, 1) + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsSingleNodeResponse, node_response), [], 1, 1 + ) code = generate_lua_lib_code( lib_name + "_2", @@ -8234,56 +8237,74 @@ async def test_function_stats(self, glide_client: GlideClient): ) response = await glide_client.function_stats() - check_function_stats_response(response, [], 2, 3) + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsSingleNodeResponse, node_response), [], 2, 3 + ) assert await glide_client.function_flush(FlushMode.SYNC) == OK response = await glide_client.function_stats() - check_function_stats_response(response, [], 0, 0) + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsSingleNodeResponse, node_response), [], 0, 0 + ) - @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("cluster_mode", [False, True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_function_stats_cluster(self, glide_client: GlideClusterClient): + async def test_function_stats_running_script( + self, request, cluster_mode, protocol, glide_client: TGlideClient + ): min_version = "7.0.0" if await check_if_server_version_lt(glide_client, min_version): return pytest.mark.skip(reason=f"Redis version required >= {min_version}") - lib_name = "functionStats_without_route" - func_name = lib_name - assert await glide_client.function_flush(FlushMode.SYNC) == OK - - # function $funcName returns first argument - code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False) - assert await glide_client.function_load(code, True) == lib_name.encode() + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = create_lua_lib_with_long_running_function(lib_name, func_name, 10, True) - response = await glide_client.function_stats() - for node_response in response.values(): - check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 1, 1 - ) + # load the library + assert await glide_client.function_load(code, replace=True) == lib_name.encode() - code = generate_lua_lib_code( - lib_name + "_2", - {func_name + "_2": "return 'OK'", func_name + "_3": "return 42"}, - False, + # create a second client to run fcall + test_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=30000 ) - assert ( - await glide_client.function_load(code, True) == (lib_name + "_2").encode() + + test_client2 = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=30000 ) - response = await glide_client.function_stats() - for node_response in response.values(): - check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 2, 3 - ) + async def endless_fcall_route_call(): + await test_client.fcall_ro(func_name, arguments=[]) - assert await glide_client.function_flush(FlushMode.SYNC) == OK + async def wait_and_function_stats(): + # it can take a few seconds for FCALL to register as running + await asyncio.sleep(3) + result = await test_client2.function_stats() + running_scripts = False + for res in result.values(): + if res.get(b"running_script"): + if running_scripts: + raise Exception("Already running script on a different node") + running_scripts = True + assert res.get(b"running_script").get(b"name") == func_name.encode() + assert res.get(b"running_script").get(b"command") == [ + b"FCALL_RO", + func_name.encode(), + b"0", + ] + assert res.get(b"running_script").get(b"duration_ms") > 0 - response = await glide_client.function_stats() - for node_response in response.values(): - check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 0, 0 - ) + assert running_scripts + + await asyncio.gather( + endless_fcall_route_call(), + wait_and_function_stats(), + ) + + await test_client.close() + await test_client2.close() @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) @@ -8311,12 +8332,12 @@ async def test_function_stats_with_routing( response = await glide_client.function_stats(route) if single_route: check_function_stats_response( - cast(TFunctionStatsResponse, response), [], 1, 1 + cast(TFunctionStatsSingleNodeResponse, response), [], 1, 1 ) else: for node_response in response.values(): check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 1, 1 + cast(TFunctionStatsSingleNodeResponse, node_response), [], 1, 1 ) code = generate_lua_lib_code( @@ -8332,12 +8353,12 @@ async def test_function_stats_with_routing( response = await glide_client.function_stats(route) if single_route: check_function_stats_response( - cast(TFunctionStatsResponse, response), [], 2, 3 + cast(TFunctionStatsSingleNodeResponse, response), [], 2, 3 ) else: for node_response in response.values(): check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 2, 3 + cast(TFunctionStatsSingleNodeResponse, node_response), [], 2, 3 ) assert await glide_client.function_flush(FlushMode.SYNC, route) == OK @@ -8345,12 +8366,12 @@ async def test_function_stats_with_routing( response = await glide_client.function_stats(route) if single_route: check_function_stats_response( - cast(TFunctionStatsResponse, response), [], 0, 0 + cast(TFunctionStatsSingleNodeResponse, response), [], 0, 0 ) else: for node_response in response.values(): check_function_stats_response( - cast(TFunctionStatsResponse, node_response), [], 0, 0 + cast(TFunctionStatsSingleNodeResponse, node_response), [], 0, 0 ) @pytest.mark.parametrize("cluster_mode", [True, False]) @@ -8379,20 +8400,10 @@ async def test_function_kill_no_write( request, cluster_mode=cluster_mode, protocol=protocol, timeout=15000 ) - # call fcall to run the function - # make sure that fcall routes to a primary node, and not a replica - # if this happens, function_kill and function_stats won't find the function and will fail - primaryRoute = SlotKeyRoute(SlotType.PRIMARY, lib_name) - async def endless_fcall_route_call(): # fcall is supposed to be killed, and will return a RequestError with pytest.raises(RequestError) as e: - if cluster_mode: - await test_client.fcall_ro_route( - func_name, arguments=[], route=primaryRoute - ) - else: - await test_client.fcall_ro(func_name, arguments=[]) + await test_client.fcall_ro(func_name, arguments=[]) assert "Script killed by user" in str(e) async def wait_and_function_kill(): diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index 96f08a7b5a..691bf98c42 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -8,7 +8,7 @@ from glide.constants import ( TClusterResponse, TFunctionListResponse, - TFunctionStatsResponse, + TFunctionStatsSingleNodeResponse, TResult, ) from glide.glide_client import TGlideClient @@ -309,7 +309,7 @@ def check_function_list_response( def check_function_stats_response( - response: TFunctionStatsResponse, + response: TFunctionStatsSingleNodeResponse, running_function: List[bytes], lib_count: int, function_count: int, @@ -318,7 +318,7 @@ def check_function_stats_response( Validate whether `FUNCTION STATS` response contains required info. Args: - response (TFunctionStatsResponse): The response from server. + response (TFunctionStatsSingleNodeResponse): The response from server. running_function (List[bytes]): Command line of running function expected. Empty, if nothing expected. lib_count (int): Expected libraries count. function_count (int): Expected functions count. diff --git a/submodules/redis-rs b/submodules/redis-rs index de53b2b5c6..b43a07e7f7 160000 --- a/submodules/redis-rs +++ b/submodules/redis-rs @@ -1 +1 @@ -Subproject commit de53b2b5c68e7ef4667e2b195eb9b1d0dd460722 +Subproject commit b43a07e7f76e3f341661b95b40df0d60ba6f89f8 From a23b6b187a8fac27bc5f231428c7b4b543abb52b Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Tue, 20 Aug 2024 14:21:37 +0300 Subject: [PATCH 194/236] Node: add binary support to lpush lpop lpopcount lrange (#2151) * Add bytes to lpush lpop lpopcount lrange Signed-off-by: lior sventitzky --------- Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 36 +++++++++++++++++++++++++++--------- node/src/Commands.ts | 11 ++++++----- node/tests/SharedTests.ts | 34 ++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 450eb9f774..68156ef365 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2030,7 +2030,10 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that a new list was created with one element * ``` */ - public async lpush(key: string, elements: string[]): Promise { + public async lpush( + key: GlideString, + elements: GlideString[], + ): Promise { return this.createWritePromise(createLPush(key, elements)); } @@ -2059,6 +2062,8 @@ export class BaseClient { * @see {@link https://valkey.io/commands/lpop/|valkey.io} for details. * * @param key - The key of the list. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The value of the first element. * If `key` does not exist null will be returned. * @@ -2076,8 +2081,11 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public async lpop(key: string): Promise { - return this.createWritePromise(createLPop(key)); + public async lpop( + key: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createLPop(key), { decoder: decoder }); } /** Removes and returns up to `count` elements of the list stored at `key`, depending on the list's length. @@ -2086,6 +2094,8 @@ export class BaseClient { * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of the popped elements will be returned depending on the list's length. * If `key` does not exist null will be returned. * @@ -2104,10 +2114,13 @@ export class BaseClient { * ``` */ public async lpopCount( - key: string, + key: GlideString, count: number, - ): Promise { - return this.createWritePromise(createLPop(key, count)); + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createLPop(key, count), { + decoder: decoder, + }); } /** Returns the specified elements of the list stored at `key`. @@ -2120,6 +2133,8 @@ export class BaseClient { * @param key - The key of the list. * @param start - The starting point of the range. * @param end - The end of the range. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns list of elements in the specified range. * If `start` exceeds the end of the list, or if `start` is greater than `end`, an empty list will be returned. * If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. @@ -2147,11 +2162,14 @@ export class BaseClient { * ``` */ public async lrange( - key: string, + key: GlideString, start: number, end: number, - ): Promise { - return this.createWritePromise(createLRange(key, start, end)); + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createLRange(key, start, end), { + decoder: decoder, + }); } /** Returns the length of the list stored at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 80206db1af..a75c3a7f4e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -838,8 +838,8 @@ export function createHGetAll(key: string): command_request.Command { * @internal */ export function createLPush( - key: string, - elements: string[], + key: GlideString, + elements: GlideString[], ): command_request.Command { return createCommand(RequestType.LPush, [key].concat(elements)); } @@ -858,10 +858,11 @@ export function createLPushX( * @internal */ export function createLPop( - key: string, + key: GlideString, count?: number, ): command_request.Command { - const args: string[] = count == undefined ? [key] : [key, count.toString()]; + const args: GlideString[] = + count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.LPop, args); } @@ -869,7 +870,7 @@ export function createLPop( * @internal */ export function createLRange( - key: string, + key: GlideString, start: number, end: number, ): command_request.Command { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0443ccb837..b13af93fe7 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1948,16 +1948,22 @@ export function runBaseTests(config: { `lpush, lpop and lrange with existing and non existing key_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); - const valueList = ["value4", "value3", "value2", "value1"]; - expect(await client.lpush(key, valueList)).toEqual(4); - expect(await client.lpop(key)).toEqual("value1"); - expect(await client.lrange(key, 0, -1)).toEqual([ + const key1 = uuidv4(); + const key2 = Buffer.from(uuidv4()); + const valueList1 = ["value4", "value3", "value2", "value1"]; + const valueList2 = ["value7", "value6", "value5"]; + const encodedValues = [ + Buffer.from("value6"), + Buffer.from("value7"), + ]; + expect(await client.lpush(key1, valueList1)).toEqual(4); + expect(await client.lpop(key1)).toEqual("value1"); + expect(await client.lrange(key1, 0, -1)).toEqual([ "value2", "value3", "value4", ]); - expect(await client.lpopCount(key, 2)).toEqual([ + expect(await client.lpopCount(key1, 2)).toEqual([ "value2", "value3", ]); @@ -1965,6 +1971,22 @@ export function runBaseTests(config: { [], ); expect(await client.lpop("nonExistingKey")).toEqual(null); + expect(await client.lpush(key2, valueList2)).toEqual(3); + expect(await client.lpop(key2, Decoder.Bytes)).toEqual( + Buffer.from("value5"), + ); + expect(await client.lrange(key2, 0, -1, Decoder.Bytes)).toEqual( + encodedValues, + ); + expect(await client.lpopCount(key2, 2, Decoder.Bytes)).toEqual( + encodedValues, + ); + expect( + await client.lpush(key2, [Buffer.from("value8")]), + ).toEqual(1); + expect(await client.lpop(key2, Decoder.Bytes)).toEqual( + Buffer.from("value8"), + ); }, protocol); }, config.timeout, From 49fdb8742917bc1dc2478e12d1aab0597e136574 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Tue, 20 Aug 2024 00:21:59 +0000 Subject: [PATCH 195/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 10 +++++----- java/THIRD_PARTY_LICENSES_JAVA | 10 +++++----- node/THIRD_PARTY_LICENSES_NODE | 14 +++++++------- python/THIRD_PARTY_LICENSES_PYTHON | 10 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 4de92d15d8..2b6d8abbb1 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -12361,7 +12361,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.157 +Package: libc:0.2.158 The following copyrights and licenses were found in the source code of this package: @@ -18262,7 +18262,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.5.0 +Package: protobuf:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -18287,7 +18287,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.5.0 +Package: protobuf-support:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -19284,7 +19284,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: redox_users:0.4.5 +Package: redox_users:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -19781,7 +19781,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.1 +Package: rustls-native-certs:0.7.2 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 aaec2fa714..6de08f9da2 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -13256,7 +13256,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.157 +Package: libc:0.2.158 The following copyrights and licenses were found in the source code of this package: @@ -19157,7 +19157,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.5.0 +Package: protobuf:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -19182,7 +19182,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.5.0 +Package: protobuf-support:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -20179,7 +20179,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: redox_users:0.4.5 +Package: redox_users:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -20676,7 +20676,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.1 +Package: rustls-native-certs:0.7.2 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 8e3c4aa182..cfc4bf93f2 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -12875,7 +12875,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.157 +Package: libc:0.2.158 The following copyrights and licenses were found in the source code of this package: @@ -18919,7 +18919,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.5.0 +Package: protobuf:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -18944,7 +18944,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.5.0 +Package: protobuf-support:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -19941,7 +19941,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: redox_users:0.4.5 +Package: redox_users:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -21125,7 +21125,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.1 +Package: rustls-native-certs:0.7.2 The following copyrights and licenses were found in the source code of this package: @@ -37493,7 +37493,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: undici-types:6.19.6 +Package: undici-types:6.19.8 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +37903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.4.0 +Package: @types:node:22.4.1 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 eb74a0cbaa..552d6baf7c 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -13027,7 +13027,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.157 +Package: libc:0.2.158 The following copyrights and licenses were found in the source code of this package: @@ -19182,7 +19182,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.5.0 +Package: protobuf:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -19207,7 +19207,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.5.0 +Package: protobuf-support:3.5.1 The following copyrights and licenses were found in the source code of this package: @@ -21349,7 +21349,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: redox_users:0.4.5 +Package: redox_users:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -21846,7 +21846,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.1 +Package: rustls-native-certs:0.7.2 The following copyrights and licenses were found in the source code of this package: From 302a79ce7e784ddfbca0607e8b3df9634e3349f5 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:31:19 +0300 Subject: [PATCH 196/236] Add Valkey8 support (#2169) --------- Signed-off-by: Shoham Elias --- .github/json_matrices/engine-matrix.json | 4 ++ .github/workflows/install-valkey/action.yml | 1 + .github/workflows/python.yml | 2 + java/integTest/build.gradle | 51 ++++++++++++-- .../test/java/glide/SharedCommandTests.java | 66 ++++++++++++++----- .../test/java/glide/cluster/CommandTests.java | 29 +++++--- .../java/glide/standalone/CommandTests.java | 48 ++++++++++---- node/tests/SharedTests.ts | 42 ++++++++---- python/python/tests/test_async_client.py | 33 ++++++---- python/python/tests/test_transaction.py | 5 +- python/python/tests/utils/utils.py | 10 +-- utils/TestUtils.ts | 23 +++++-- utils/cluster_manager.py | 23 ++++++- 13 files changed, 255 insertions(+), 82 deletions(-) diff --git a/.github/json_matrices/engine-matrix.json b/.github/json_matrices/engine-matrix.json index f20f0c955e..bf755b782e 100644 --- a/.github/json_matrices/engine-matrix.json +++ b/.github/json_matrices/engine-matrix.json @@ -2,5 +2,9 @@ { "type": "valkey", "version": "7.2.5" + }, + { + "type": "valkey", + "version": "8.0.0-rc1" } ] diff --git a/.github/workflows/install-valkey/action.yml b/.github/workflows/install-valkey/action.yml index f15a875c03..74c75572a4 100644 --- a/.github/workflows/install-valkey/action.yml +++ b/.github/workflows/install-valkey/action.yml @@ -62,6 +62,7 @@ runs: echo 'export PATH=/usr/local/bin:$PATH' >>~/.bash_profile - name: Verify Valkey installation and symlinks + if: ${{ !contains(inputs.engine-version, '-rc') }} shell: bash run: | # In Valkey releases, the engine is built with symlinks from valkey-server and valkey-cli diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 7e453178a6..6bd1e23d1d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -15,6 +15,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + - .github/json_matrices/engine-matrix.json pull_request: paths: @@ -29,6 +30,7 @@ on: - .github/workflows/lint-rust/action.yml - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json + - .github/json_matrices/engine-matrix.json workflow_dispatch: concurrency: diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index d6c7593820..ec7c4a2805 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -103,12 +103,53 @@ tasks.register('startStandalone') { tasks.register('getServerVersion') { doLast { - new ByteArrayOutputStream().withStream { os -> - exec { - commandLine 'redis-server', '-v' - standardOutput = os + def detectedVersion + def output = new ByteArrayOutputStream() + + // Helper method to find the full path of a command + def findFullPath = { command -> + def pathOutput = new ByteArrayOutputStream() + try { + exec { + commandLine 'which', command // Use 'where' for Windows + standardOutput = pathOutput + } + return pathOutput.toString().trim() + } catch (Exception e) { + println "Failed to find path for ${command}: ${e.message}" + return "" + } + } + + // Get full paths + def valkeyPath = findFullPath('valkey-server') + def redisPath = findFullPath('redis-server') + + def tryGetVersion = { serverPath -> + try { + exec { + commandLine serverPath, '-v' + standardOutput = output + } + return output.toString() + } catch (Exception e) { + println "Failed to execute ${serverPath}: ${e.message}" + return "" } - serverVersion = extractServerVersion(os.toString()) + } + + // Try valkey-server first, then redis-server if it fails + def versionOutput = tryGetVersion(valkeyPath) + if (versionOutput.isEmpty() && !redisPath.isEmpty()) { + versionOutput = tryGetVersion(redisPath) + } + + if (!versionOutput.isEmpty()) { + detectedVersion = extractServerVersion(versionOutput) + println "Detected server version: ${detectedVersion}" + serverVersion = detectedVersion + } else { + throw new GradleException("Failed to retrieve the server version.") } } } diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 76b367a773..aab628e7de 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -13774,9 +13774,14 @@ public void sscan(BaseClient client) { assertDeepEquals(new String[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.sscan(key1, "-1").get(); - assertEquals(initialCursor, result[resultCursorIndex]); - assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sscan(key1, "-1").get()); + } else { + result = client.sscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.sadd(key1, charMembers).get()); @@ -13910,9 +13915,14 @@ public void sscan_binary(BaseClient client) { assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.sscan(key1, gs("-1")).get(); - assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); - assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sscan(key1, gs("-1")).get()); + } else { + result = client.sscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.sadd(key1, charMembers).get()); @@ -14059,9 +14069,14 @@ public void zscan(BaseClient client) { assertDeepEquals(new String[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.zscan(key1, "-1").get(); - assertEquals(initialCursor, result[resultCursorIndex]); - assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zscan(key1, "-1").get()); + } else { + result = client.zscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.zadd(key1, charMap).get()); @@ -14240,9 +14255,14 @@ public void zscan_binary(BaseClient client) { assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.zscan(key1, gs("-1")).get(); - assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); - assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zscan(key1, gs("-1")).get()); + } else { + result = client.zscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.zadd(key1.toString(), charMap_strings).get()); @@ -14425,9 +14445,14 @@ public void hscan(BaseClient client) { assertDeepEquals(new String[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.hscan(key1, "-1").get(); - assertEquals(initialCursor, result[resultCursorIndex]); - assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hscan(key1, "-1").get()); + } else { + result = client.hscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.hset(key1, charMap).get()); @@ -14589,9 +14614,14 @@ public void hscan_binary(BaseClient client) { assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); // Negative cursor - result = client.hscan(key1, gs("-1")).get(); - assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); - assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hscan(key1, gs("-1")).get()); + } else { + result = client.hscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + } // Result contains the whole set assertEquals(charMembers.length, client.hset(key1, charMap).get()); diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 83c6058d26..989034d10a 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -1051,15 +1051,20 @@ public void flushall() { assertEquals(OK, clusterClient.flushall(ASYNC, route).get()); var replicaRoute = new SlotKeyRoute("key", REPLICA); - // command should fail on a replica, because it is read-only - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> clusterClient.flushall(replicaRoute).get()); - assertInstanceOf(RequestException.class, executionException.getCause()); - assertTrue( - executionException - .getMessage() - .toLowerCase() - .contains("can't write against a read only replica")); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + // Since Valkey 8.0.0 flushall can run on replicas + assertEquals(OK, clusterClient.flushall(route).get()); + } else { + // command should fail on a replica, because it is read-only + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.flushall(replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("can't write against a read only replica")); + } } // TODO: add a binary version of this test @@ -1640,6 +1645,9 @@ public void fcall_binary_with_keys(String prefix) { @Test public void fcall_readonly_function() { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + assumeTrue( + !SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0"), + "Temporary disabeling this test on valkey 8"); String libName = "fcall_readonly_function"; // intentionally using a REPLICA route @@ -1695,6 +1703,9 @@ public void fcall_readonly_function() { @Test public void fcall_readonly_binary_function() { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + assumeTrue( + !SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0"), + "Temporary disabeling this test on valkey 8"); String libName = "fcall_readonly_function"; // intentionally using a REPLICA route diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index eeb8f6660d..458c8edff1 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -1547,9 +1547,14 @@ public void scan() { assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); // Negative cursor - Object[] negativeResult = regularClient.scan("-1").get(); - assertEquals(initialCursor, negativeResult[resultCursorIndex]); - assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.scan("-1").get()); + } else { + Object[] negativeResult = regularClient.scan("-1").get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + } // Add keys to the database using mset regularClient.mset(keys).get(); @@ -1601,9 +1606,14 @@ public void scan_binary() { assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); // Negative cursor - Object[] negativeResult = regularClient.scan(gs("-1")).get(); - assertEquals(initialCursor, negativeResult[resultCursorIndex]); - assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.scan(gs("-1")).get()); + } else { + Object[] negativeResult = regularClient.scan(gs("-1")).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + } // Add keys to the database using mset regularClient.msetBinary(keys).get(); @@ -1664,9 +1674,16 @@ public void scan_with_options() { assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); // Negative cursor - Object[] negativeResult = regularClient.scan("-1", options).get(); - assertEquals(initialCursor, negativeResult[resultCursorIndex]); - assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + final ScanOptions finalOptions = options; + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> regularClient.scan("-1", finalOptions).get()); + } else { + Object[] negativeResult = regularClient.scan("-1", options).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + } // scan for strings by match pattern: options = @@ -1746,9 +1763,16 @@ public void scan_binary_with_options() { assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); // Negative cursor - Object[] negativeResult = regularClient.scan(gs("-1"), options).get(); - assertEquals(initialCursor, negativeResult[resultCursorIndex]); - assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")) { + final ScanOptions finalOptions = options; + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> regularClient.scan(gs("-1"), finalOptions).get()); + } else { + Object[] negativeResult = regularClient.scan(gs("-1"), options).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + } // scan for strings by match pattern: options = diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b13af93fe7..3d815e3b57 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1504,7 +1504,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `hscan and sscan empty set, negative cursor, negative count, and non-hash key exception tests`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); const initialCursor = "0"; @@ -1521,13 +1521,23 @@ export function runBaseTests(config: { expect(result[resultCollectionIndex]).toEqual([]); // Negative cursor - result = await client.hscan(key1, "-1"); - expect(result[resultCursorIndex]).toEqual(initialCursor); - expect(result[resultCollectionIndex]).toEqual([]); + if (cluster.checkIfServerVersionLessThan("7.9.0")) { + result = await client.hscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + + result = await client.sscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + } else { + await expect(client.hscan(key1, "-1")).rejects.toThrow( + RequestError, + ); - result = await client.sscan(key1, "-1"); - expect(result[resultCursorIndex]).toEqual(initialCursor); - expect(result[resultCollectionIndex]).toEqual([]); + await expect(client.sscan(key1, "-1")).rejects.toThrow( + RequestError, + ); + } // Exceptions // Non-hash key @@ -8485,7 +8495,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zscan test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); const initialCursor = "0"; @@ -8516,9 +8526,19 @@ export function runBaseTests(config: { expect(result[resultCollectionIndex]).toEqual([]); // Negative cursor - result = await client.zscan(key1, "-1"); - expect(result[resultCursorIndex]).toEqual(initialCursor); - expect(result[resultCollectionIndex]).toEqual([]); + if (cluster.checkIfServerVersionLessThan("7.9.0")) { + result = await client.zscan(key1, "-1"); + expect(result[resultCursorIndex]).toEqual(initialCursor); + expect(result[resultCollectionIndex]).toEqual([]); + } else { + try { + expect(await client.zscan(key1, "-1")).toThrow(); + } catch (e) { + expect((e as Error).message).toMatch( + "ResponseError: invalid cursor", + ); + } + } // Result contains the whole set expect(await client.zadd(key1, charMap)).toEqual( diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index ce8008856a..a5026e70dc 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -412,9 +412,6 @@ async def test_info_server_replication(self, glide_client: TGlideClient): info_res = get_first_result(await glide_client.info([InfoSection.SERVER])) info = info_res.decode() assert "# Server" in info - cluster_mode = parse_info_response(info_res)["redis_mode"] - expected_cluster_mode = isinstance(glide_client, GlideClusterClient) - assert cluster_mode == "cluster" if expected_cluster_mode else "standalone" info = get_first_result( await glide_client.info([InfoSection.REPLICATION]) ).decode() @@ -9834,9 +9831,13 @@ async def test_sscan(self, glide_client: GlideClusterClient): assert result[result_collection_index] == [] # Negative cursor - result = await glide_client.sscan(key1, "-1") - assert result[result_cursor_index] == initial_cursor.encode() - assert result[result_collection_index] == [] + if await check_if_server_version_lt(glide_client, "7.9.0"): + result = await glide_client.sscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + else: + with pytest.raises(RequestError): + await glide_client.sscan(key2, "-1") # Result contains the whole set assert await glide_client.sadd(key1, char_members) == len(char_members) @@ -9944,9 +9945,13 @@ async def test_zscan(self, glide_client: GlideClusterClient): assert result[result_collection_index] == [] # Negative cursor - result = await glide_client.zscan(key1, "-1") - assert result[result_cursor_index] == initial_cursor.encode() - assert result[result_collection_index] == [] + if await check_if_server_version_lt(glide_client, "7.9.0"): + result = await glide_client.zscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + else: + with pytest.raises(RequestError): + await glide_client.zscan(key2, "-1") # Result contains the whole set assert await glide_client.zadd(key1, char_map) == len(char_map) @@ -10057,9 +10062,13 @@ async def test_hscan(self, glide_client: GlideClusterClient): assert result[result_collection_index] == [] # Negative cursor - result = await glide_client.hscan(key1, "-1") - assert result[result_cursor_index] == initial_cursor.encode() - assert result[result_collection_index] == [] + if await check_if_server_version_lt(glide_client, "7.9.0"): + result = await glide_client.hscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + else: + with pytest.raises(RequestError): + await glide_client.hscan(key2, "-1") # Result contains the whole set assert await glide_client.hset(key1, char_map) == len(char_map) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 9d8f09f865..a279b76f87 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -842,9 +842,8 @@ async def test_transaction_custom_unsupported_command( transaction.custom_command(["WATCH", key]) with pytest.raises(RequestError) as e: await self.exec_transaction(glide_client, transaction) - assert "WATCH inside MULTI is not allowed" in str( - e - ) # TODO : add an assert on EXEC ABORT + + assert "not allowed" in str(e) # TODO : add an assert on EXEC ABORT @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index 691bf98c42..ff156974f5 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -77,11 +77,11 @@ def get_random_string(length): async def check_if_server_version_lt(client: TGlideClient, min_version: str) -> bool: - # TODO: change it to pytest fixture after we'll implement a sync client - info_str = await client.info([InfoSection.SERVER]) - server_version = parse_info_response(info_str).get("redis_version") - assert server_version is not None - return version.parse(server_version) < version.parse(min_version) + # TODO: change to pytest fixture after sync client is implemented + info = parse_info_response(await client.info([InfoSection.SERVER])) + version_str = info.get("valkey_version") or info.get("redis_version") + assert version_str is not None, "Server version not found in INFO response" + return version.parse(version_str) < version.parse(min_version) def compare_maps( diff --git a/utils/TestUtils.ts b/utils/TestUtils.ts index 84f9acd146..0a6f581088 100644 --- a/utils/TestUtils.ts +++ b/utils/TestUtils.ts @@ -51,15 +51,26 @@ export class RedisCluster { } private static async detectVersion(): Promise { - return new Promise((resolve, reject) => - exec(`redis-server -v`, (error, stdout) => { + return new Promise((resolve, reject) => { + const extractVersion = (stdout: string): string => + stdout.split("v=")[1].split(" ")[0]; + + // First, try with `valkey-server -v` + exec("valkey-server -v", (error, stdout) => { if (error) { - reject(error); + // If `valkey-server` fails, try `redis-server -v` + exec("redis-server -v", (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(extractVersion(stdout)); + } + }); } else { - resolve(stdout.split("v=")[1].split(" ")[0]); + resolve(extractVersion(stdout)); } - }) - ); + }); + }); } public static createCluster( diff --git a/utils/cluster_manager.py b/utils/cluster_manager.py index 8eedcb0e4d..03adcaba00 100644 --- a/utils/cluster_manager.py +++ b/utils/cluster_manager.py @@ -280,11 +280,31 @@ def start_redis_server( ) -> Tuple[RedisServer, str]: port = port if port else next_free_port() logging.debug(f"Creating server {host}:{port}") + # Create sub-folder for each node node_folder = f"{cluster_folder}/{port}" Path(node_folder).mkdir(exist_ok=True) + + # Determine which server to use by checking `valkey-server` and `redis-server` + def get_server_command() -> str: + for server in ["valkey-server", "redis-server"]: + try: + result = subprocess.run( + ["which", server], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode == 0: + return server + except Exception as e: + logging.error(f"Error checking {server}: {e}") + raise Exception("Neither valkey-server nor redis-server found in the system.") + + server_name = get_server_command() + # Define command arguments cmd_args = [ - "redis-server", + server_name, f"{'--tls-port' if tls else '--port'}", str(port), "--cluster-enabled", @@ -315,6 +335,7 @@ def start_redis_server( raise Exception( f"Failed to execute command: {str(p.args)}\n Return code: {p.returncode}\n Error: {err}" ) + server = RedisServer(host, port) return server, node_folder From 866d47ac9050a60b9538284650d12cfc51c36a7e Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:46:38 +0300 Subject: [PATCH 197/236] Add modules testing CI (#2162) --------- Signed-off-by: Shoham Elias --- .github/workflows/python.yml | 79 ++++++++++++++++++- .../tests/tests_server_modules/test_json.py | 36 ++++----- 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 6bd1e23d1d..2511c8c1f4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -16,6 +16,7 @@ on: - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json - .github/json_matrices/engine-matrix.json + - .github/workflows/start-self-hosted-runner/action.yml pull_request: paths: @@ -31,6 +32,7 @@ on: - .github/workflows/install-valkey/action.yml - .github/json_matrices/build-matrix.json - .github/json_matrices/engine-matrix.json + - .github/workflows/start-self-hosted-runner/action.yml workflow_dispatch: concurrency: @@ -39,6 +41,8 @@ concurrency: permissions: contents: read + # Allows the GITHUB_TOKEN to make an API call to generate an OIDC token. + id-token: write jobs: load-engine-matrix: @@ -156,7 +160,7 @@ jobs: OS: ubuntu, RUNNER: ubuntu-latest, TARGET: x86_64-unknown-linux-gnu - } + } # - { # OS: macos, # RUNNER: macos-latest, @@ -284,3 +288,76 @@ jobs: name: smoke-test-report-amazon-linux path: | python/python/tests/pytest_report.html + + start-self-hosted-runner: + if: github.event.pull_request.head.repo.owner.login == 'valkey-io' + runs-on: ubuntu-latest + environment: AWS_ACTIONS + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Start self hosted EC2 runner + uses: ./.github/workflows/start-self-hosted-runner + with: + role-to-assume: ${{ secrets.ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + ec2-instance-id: ${{ secrets.AWS_EC2_INSTANCE_ID }} + + test-modules: + needs: [start-self-hosted-runner, load-engine-matrix] + name: Running Module Tests + runs-on: ${{ matrix.host.RUNNER }} + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + python: + - "3.12" + host: + - { + OS: "ubuntu", + NAMED_OS: "linux", + RUNNER: ["self-hosted", "Linux", "ARM64"], + TARGET: "aarch64-unknown-linux-gnu", + } + + steps: + - name: Setup self-hosted runner access + if: ${{ contains(matrix.host.RUNNER, 'self-hosted') }} + run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Python for self-hosted Ubuntu runners + run: | + sudo apt update -y + sudo apt upgrade -y + sudo apt install python3 python3-venv python3-pip -y + + - name: Build Python wrapper + uses: ./.github/workflows/build-python-wrapper + with: + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} + github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: ${{ matrix.engine.version }} + + - name: Test with pytest + working-directory: ./python + run: | + source .env/bin/activate + cd python/tests/ + pytest --asyncio-mode=auto --tls --cluster-endpoints=${{ secrets.MEMDB_MODULES_ENDPOINT }} -k server_modules --html=pytest_report.html --self-contained-html + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: smoke-test-report-amazon-linux + path: | + python/python/tests/pytest_report.html diff --git a/python/python/tests/tests_server_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py index c6ee1a72f4..2c9ddabce4 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -15,12 +15,6 @@ @pytest.mark.asyncio class TestJson: - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_module_is_loaded(self, glide_client: TGlideClient): - res = parse_info_response(await glide_client.info([InfoSection.MODULES])) - assert "ReJSON" in res["module"] - @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_json_set_get(self, glide_client: TGlideClient): @@ -30,15 +24,15 @@ async def test_json_set_get(self, glide_client: TGlideClient): assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK result = await json.get(glide_client, key, ".") - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == json_value result = await json.get(glide_client, key, ["$.a", "$.b"]) - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == {"$.a": [1.0], "$.b": [2]} assert await json.get(glide_client, "non_existing_key", "$") is None - assert await json.get(glide_client, key, "$.d") == "[]" + assert await json.get(glide_client, key, "$.d") == b"[]" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) @@ -56,16 +50,16 @@ async def test_json_set_get_multiple_values(self, glide_client: TGlideClient): ) result = await json.get(glide_client, key, "$..c") - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == [True, 1, 2] result = await json.get(glide_client, key, ["$..c", "$.c"]) - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == {"$..c": [True, 1, 2], "$.c": [True]} assert await json.set(glide_client, key, "$..c", '"new_value"') == OK result = await json.get(glide_client, key, "$..c") - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == ["new_value"] * 3 @pytest.mark.parametrize("cluster_mode", [True, False]) @@ -105,7 +99,7 @@ async def test_json_set_conditional_set(self, glide_client: TGlideClient): is None ) - assert await json.get(glide_client, key, ".a") == "1.0" + assert await json.get(glide_client, key, ".a") == b"1.0" assert ( await json.set( @@ -118,7 +112,7 @@ async def test_json_set_conditional_set(self, glide_client: TGlideClient): == OK ) - assert await json.get(glide_client, key, ".a") == "4.5" + assert await json.get(glide_client, key, ".a") == b"4.5" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) @@ -138,16 +132,14 @@ async def test_json_get_formatting(self, glide_client: TGlideClient): glide_client, key, "$", JsonGetOptions(indent=" ", newline="\n", space=" ") ) - expected_result = '[\n {\n "a": 1.0,\n "b": 2,\n "c": {\n "d": 3,\n "e": 4\n }\n }\n]' + expected_result = b'[\n {\n "a": 1.0,\n "b": 2,\n "c": {\n "d": 3,\n "e": 4\n }\n }\n]' assert result == expected_result result = await json.get( glide_client, key, "$", JsonGetOptions(indent="~", newline="\n", space="*") ) - expected_result = ( - '[\n~{\n~~"a":*1.0,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]' - ) + expected_result = b'[\n~{\n~~"a":*1.0,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]' assert result == expected_result @pytest.mark.parametrize("cluster_mode", [True, False]) @@ -159,10 +151,10 @@ async def test_del(self, glide_client: TGlideClient): assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK assert await json.delete(glide_client, key, "$..a") == 2 - assert await json.get(glide_client, key, "$..a") == "[]" + assert await json.get(glide_client, key, "$..a") == b"[]" result = await json.get(glide_client, key, "$") - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] assert await json.delete(glide_client, key, "$") == 1 @@ -178,10 +170,10 @@ async def test_forget(self, glide_client: TGlideClient): assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK assert await json.forget(glide_client, key, "$..a") == 2 - assert await json.get(glide_client, key, "$..a") == "[]" + assert await json.get(glide_client, key, "$..a") == b"[]" result = await json.get(glide_client, key, "$") - assert isinstance(result, str) + assert isinstance(result, bytes) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] assert await json.forget(glide_client, key, "$") == 1 From c397ce78fa4b49e13c8a6ac8fb7c8ad2bcd8781c Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:14:37 -0700 Subject: [PATCH 198/236] Node: add command XACK (#2112) Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 31 ++++++++++ node/src/Commands.ts | 11 ++++ node/src/Transaction.ts | 17 ++++++ node/tests/SharedTests.ts | 113 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 6 files changed, 175 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af4e2672e4..0d4d56abe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) * Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) * Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) +* Node: Added XACK commands ([#2112](https://github.com/valkey-io/valkey-glide/pull/2112)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 68156ef365..1c4b24c87e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -171,6 +171,7 @@ import { createUnlink, createWait, createWatch, + createXAck, createXAdd, createXAutoClaim, createXClaim, @@ -5183,6 +5184,36 @@ export class BaseClient { preferReplica: connection_request.ReadFrom.PreferReplica, }; + /** + * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. + * This command should be called on a pending message so that such message does not get processed again. + * + * @see {@link https://valkey.io/commands/xack/|valkey.io} for details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param ids - An array of entry ids. + * @returns The number of messages that were successfully acknowledged. + * + * @example + * ```typescript + *
      {@code
      +     * const entryId = await client.xadd("mystream", ["myfield", "mydata"]);
      +     * // read messages from streamId
      +     * const readResult = await client.xreadgroup(["myfield", "mydata"], "mygroup", "my0consumer");
      +     * // acknowledge messages on stream
      +     * console.log(await client.xack("mystream", "mygroup", [entryId])); // Output: 1L
      +     * 
      + * ``` + */ + public async xack( + key: string, + group: string, + ids: string[], + ): Promise { + return this.createWritePromise(createXAck(key, group, ids)); + } + /** Returns the element at index `index` in the list stored at `key`. * 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. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index a75c3a7f4e..3f127fd1bf 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3942,3 +3942,14 @@ export function createGetEx( return createCommand(RequestType.GetEx, args); } + +/** + * @internal + */ +export function createXAck( + key: string, + group: string, + ids: string[], +): command_request.Command { + return createCommand(RequestType.XAck, [key, group, ...ids]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8d5d633fc9..2fd3fa5e02 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -207,6 +207,7 @@ import { createType, createUnlink, createWait, + createXAck, createXAdd, createXAutoClaim, createXClaim, @@ -2892,6 +2893,22 @@ export class BaseTransaction> { ); } + /** + * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. + * This command should be called on a pending message so that such message does not get processed again. + * + * @see {@link https://valkey.io/commands/xack/|valkey.io} for details. + * + * @param key - The key of the stream. + * @param group - The consumer group name. + * @param ids - An array of entry ids. + * + * Command Response - The number of messages that were successfully acknowledged. + */ + public xack(key: string, group: string, ids: string[]): T { + return this.addAndReturn(createXAck(key, group, ids)); + } + /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 3d815e3b57..62bf853a71 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -9834,6 +9834,119 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xack test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = "{testKey}:1-" + uuidv4(); + const nonExistingKey = "{testKey}:2-" + uuidv4(); + const string_key = "{testKey}:3-" + uuidv4(); + const groupName = uuidv4(); + const consumerName = uuidv4(); + const stream_id0 = "0"; + const stream_id1_0 = "1-0"; + const stream_id1_1 = "1-1"; + const stream_id1_2 = "1-2"; + + // setup: add 2 entries to the stream, create consumer group and read to mark them as pending + expect( + await client.xadd(key, [["f0", "v0"]], { + id: stream_id1_0, + }), + ).toEqual(stream_id1_0); + expect( + await client.xadd(key, [["f1", "v1"]], { + id: stream_id1_1, + }), + ).toEqual(stream_id1_1); + expect( + await client.xgroupCreate(key, groupName, stream_id0), + ).toBe("OK"); + expect( + await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }), + ).toEqual({ + [key]: { + [stream_id1_0]: [["f0", "v0"]], + [stream_id1_1]: [["f1", "v1"]], + }, + }); + + // add one more entry + expect( + await client.xadd(key, [["f2", "v2"]], { + id: stream_id1_2, + }), + ).toEqual(stream_id1_2); + + // acknowledge the first 2 entries + expect( + await client.xack(key, groupName, [ + stream_id1_0, + stream_id1_1, + ]), + ).toBe(2); + + // attempt to acknowledge the first 2 entries again, returns 0 since they were already acknowledged + expect( + await client.xack(key, groupName, [ + stream_id1_0, + stream_id1_1, + ]), + ).toBe(0); + + // read the last unacknowledged entry + expect( + await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }), + ).toEqual({ [key]: { [stream_id1_2]: [["f2", "v2"]] } }); + + // deleting the consumer, returns 1 since the last entry still hasn't been acknowledged + expect( + await client.xgroupDelConsumer( + key, + groupName, + consumerName, + ), + ).toBe(1); + + // attempt to acknowledge a non-existing key, returns 0 + expect( + await client.xack(nonExistingKey, groupName, [ + stream_id1_0, + ]), + ).toBe(0); + + // attempt to acknowledge a non-existing group name, returns 0 + expect( + await client.xack(key, "nonExistingGroup", [stream_id1_0]), + ).toBe(0); + + // attempt to acknowledge a non-existing ID, returns 0 + expect(await client.xack(key, groupName, ["99-99"])).toBe(0); + + // invalid argument - ID list must not be empty + await expect(client.xack(key, groupName, [])).rejects.toThrow( + RequestError, + ); + + // invalid argument - invalid stream ID format + await expect( + client.xack(key, groupName, ["invalid stream ID format"]), + ).rejects.toThrow(RequestError); + + // key exists, but is not a stream + expect(await client.set(string_key, "xack")).toBe("OK"); + await expect( + client.xack(string_key, groupName, [stream_id1_0]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lmpop test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4605f89aa9..b6680eb685 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1215,6 +1215,8 @@ export async function transactionTest( ]); } + baseTransaction.xack(key9, groupName1, ["0-3"]); + responseData.push(["xack(key9, groupName1, ['0-3'])", 0]); baseTransaction.xgroupDelConsumer(key9, groupName1, consumer); responseData.push(["xgroupDelConsumer(key9, groupName1, consumer)", 1]); baseTransaction.xgroupDestroy(key9, groupName1); From 681d36056267e9afc4c7ec1ffe17c6e49d7b1344 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Tue, 20 Aug 2024 10:24:50 -0700 Subject: [PATCH 199/236] Java: Fix benchmarkApp to support port (#2170) * Java: Add port and examples for benchmark app Signed-off-by: Andrew Carbonetto --- java/README.md | 10 ++++++++++ .../main/java/glide/benchmarks/BenchmarkingApp.java | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/java/README.md b/java/README.md index 3c1a8cbe1d..9b14dab87d 100644 --- a/java/README.md +++ b/java/README.md @@ -271,11 +271,21 @@ For more examples, you can refer to the test folder [unit tests](./client/src/te You can run benchmarks using `./gradlew run`. You can set arguments using the args flag like: +Returns the command help output ```shell ./gradlew run --args="--help" +``` + +Runs all benchmark clients against a local instance with TLS enabled using data sizing 100 and 1000 bytes, 10 and 100 concurrent tasks, 1 and 5 parallel clients. +```shell ./gradlew run --args="--resultsFile=output --dataSize \"100 1000\" --concurrentTasks \"10 100\" --clients all --host localhost --port 6279 --clientCount \"1 5\" --tls" ``` +Runs GLIDE client against a local cluster instance on port 52756 using data sizing 4000 bytes, and 1000 concurrent tasks. +```shell +./gradlew run --args="--resultsFile=output --dataSize \"4000\" --concurrentTasks \"1000\" --clients glide --host 127.0.0.1 --port 52746 --clusterModeEnabled" +``` + The following arguments are accepted: * `resultsFile`: the results output file * `concurrentTasks`: number of concurrent tasks diff --git a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java index 31ab7bbd13..7f7f2de254 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java @@ -176,6 +176,10 @@ private static RunConfiguration verifyOptions(CommandLine line) throws ParseExce runConfiguration.host = line.getOptionValue("host"); } + if (line.hasOption("port")) { + runConfiguration.port = Integer.parseInt(line.getOptionValue("port")); + } + if (line.hasOption("clientCount")) { runConfiguration.clientCount = parseIntListOption(line.getOptionValue("clientCount")); } From d3a9e539210e9db842b169dc4b944ea63fa442ac Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:36:48 -0700 Subject: [PATCH 200/236] Node: Add command XGROUP SETID (#2135) Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 35 +++++++++++++ node/src/Commands.ts | 19 +++++++ node/src/Transaction.ts | 25 +++++++++ node/tests/SharedTests.ts | 102 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 6 files changed, 184 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4d56abe7..a401ce8748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ * Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) * Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) * Node: Added XACK commands ([#2112](https://github.com/valkey-io/valkey-glide/pull/2112)) +* Node: Added XGROUP SETID command ([#2135]((https://github.com/valkey-io/valkey-glide/pull/2135)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1c4b24c87e..563db515ca 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -220,6 +220,7 @@ import { createZScore, createZUnion, createZUnionStore, + createXGroupSetid, } from "./Commands"; import { ClosingError, @@ -5214,6 +5215,40 @@ export class BaseClient { return this.createWritePromise(createXAck(key, group, ids)); } + /** + * Sets the last delivered ID for a consumer group. + * + * @see {@link https://valkey.io/commands/xgroup-setid|valkey.io} for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param id - The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @param entriesRead - (Optional) A value representing the number of stream entries already read by the group. + * This option can only be specified if you are using Valkey version 7.0.0 or above. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @returns `"OK"`. + * + * * @example + * ```typescript + * console.log(await client.xgroupSetId("mystream", "mygroup", "0", 1L)); // Output is "OK" + * ``` + */ + public async xgroupSetId( + key: string, + groupName: string, + id: string, + entriesRead?: number, + decoder?: Decoder, + ): Promise<"OK"> { + return this.createWritePromise( + createXGroupSetid(key, groupName, id, entriesRead), + { + decoder: decoder, + }, + ); + } + /** Returns the element at index `index` in the list stored at `key`. * 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. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 3f127fd1bf..09bb359ecf 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3953,3 +3953,22 @@ export function createXAck( ): command_request.Command { return createCommand(RequestType.XAck, [key, group, ...ids]); } + +/** + * @internal + */ +export function createXGroupSetid( + key: string, + groupName: string, + id: string, + entriesRead?: number, +): command_request.Command { + const args = [key, groupName, id]; + + if (entriesRead !== undefined) { + args.push("ENTRIESREAD"); + args.push(entriesRead.toString()); + } + + return createCommand(RequestType.XGroupSetId, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2fd3fa5e02..993cdf703b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -256,6 +256,7 @@ import { createZScore, createZUnion, createZUnionStore, + createXGroupSetid, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2909,6 +2910,30 @@ export class BaseTransaction> { return this.addAndReturn(createXAck(key, group, ids)); } + /** + * Sets the last delivered ID for a consumer group. + * + * @see {@link https://valkey.io/commands/xgroup-setid|valkey.io} for more details. + * + * @param key - The key of the stream. + * @param groupName - The consumer group name. + * @param id - The stream entry ID that should be set as the last delivered ID for the consumer group. + * @param entriesRead - (Optional) A value representing the number of stream entries already read by the group. + * This option can only be specified if you are using Valkey version 7.0.0 or above. + * + * Command Response - `"OK"`. + */ + public xgroupSetId( + key: string, + groupName: string, + id: string, + entriesRead?: number, + ): T { + return this.addAndReturn( + createXGroupSetid(key, groupName, id, entriesRead), + ); + } + /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 62bf853a71..19a8f09493 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -9502,6 +9502,108 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `xgroupSetId test %p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + const key = "testKey" + uuidv4(); + const nonExistingKey = "group" + uuidv4(); + const stringKey = "testKey" + uuidv4(); + const groupName = uuidv4(); + const consumerName = uuidv4(); + const streamid0 = "0"; + const streamid1_0 = "1-0"; + const streamid1_1 = "1-1"; + const streamid1_2 = "1-2"; + + // Setup: Create stream with 3 entries, create consumer group, read entries to add them to the Pending Entries List + expect( + await client.xadd(key, [["f0", "v0"]], { id: streamid1_0 }), + ).toBe(streamid1_0); + expect( + await client.xadd(key, [["f1", "v1"]], { id: streamid1_1 }), + ).toBe(streamid1_1); + expect( + await client.xadd(key, [["f2", "v2"]], { id: streamid1_2 }), + ).toBe(streamid1_2); + + expect( + await client.xgroupCreate(key, groupName, streamid0), + ).toBe("OK"); + + expect( + await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }), + ).toEqual({ + [key]: { + [streamid1_0]: [["f0", "v0"]], + [streamid1_1]: [["f1", "v1"]], + [streamid1_2]: [["f2", "v2"]], + }, + }); + + // Sanity check: xreadgroup should not return more entries since they're all already in the + // Pending Entries List. + expect( + await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }), + ).toBeNull(); + + // Reset the last delivered ID for the consumer group to "1-1" + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect( + await client.xgroupSetId(key, groupName, streamid1_1), + ).toBe("OK"); + } else { + expect( + await client.xgroupSetId( + key, + groupName, + streamid1_1, + 1, + ), + ).toBe("OK"); + } + + // xreadgroup should only return entry 1-2 since we reset the last delivered ID to 1-1 + const newResult = await client.xreadgroup( + groupName, + consumerName, + { [key]: ">" }, + ); + expect(newResult).toEqual({ + [key]: { + [streamid1_2]: [["f2", "v2"]], + }, + }); + + // An error is raised if XGROUP SETID is called with a non-existing key + await expect( + client.xgroupSetId(nonExistingKey, groupName, streamid0), + ).rejects.toThrow(RequestError); + + // An error is raised if XGROUP SETID is called with a non-existing group + await expect( + client.xgroupSetId(key, "non_existing_group", streamid0), + ).rejects.toThrow(RequestError); + + // Setting the ID to a non-existing ID is allowed + expect(await client.xgroupSetId(key, groupName, "99-99")).toBe( + "OK", + ); + + // key exists, but is not a stream + expect(await client.set(stringKey, "xgroup setid")).toBe("OK"); + await expect( + client.xgroupSetId(stringKey, groupName, streamid1_0), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `xpending test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index b6680eb685..5dbb9544d1 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1217,6 +1217,8 @@ export async function transactionTest( baseTransaction.xack(key9, groupName1, ["0-3"]); responseData.push(["xack(key9, groupName1, ['0-3'])", 0]); + baseTransaction.xgroupSetId(key9, groupName1, "0-2"); + responseData.push(["xgroupSetId(key9, groupName1, '0-2')", "OK"]); baseTransaction.xgroupDelConsumer(key9, groupName1, consumer); responseData.push(["xgroupDelConsumer(key9, groupName1, consumer)", 1]); baseTransaction.xgroupDestroy(key9, groupName1); From 1ae433140ebdaed73013ee6a5e893a5d6cd519d2 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 20 Aug 2024 14:31:58 -0700 Subject: [PATCH 201/236] Update bug template. (#2155) Signed-off-by: Yury-Fridlyand --- .github/ISSUE_TEMPLATE/bug-report.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 0244578094..5f63253d51 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -95,6 +95,10 @@ body: options: - TypeScript - Python + - Java + - Rust + - Go + - .Net validations: required: true @@ -111,7 +115,7 @@ body: attributes: label: Cluster information description: | - Cluster information, cluster topology, number of shards, number of replicas, used data types. + Cluster information, cluster topology, number of shards, number of replicas, used data types. validations: required: false From 56dead090a9494817f7f9160712ca1c76fe7f6a5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 20 Aug 2024 17:05:06 -0700 Subject: [PATCH 202/236] Node: `FUNCTION DUMP` and `FUNCTION RESTORE` in transaction (#2173) Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 3 +- node/src/BaseClient.ts | 2 +- node/src/Transaction.ts | 31 +++++++++++++++++ node/tests/GlideClient.test.ts | 40 ++++++++++++++++++++++ node/tests/GlideClusterClient.test.ts | 48 +++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a401ce8748..730c391284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ #### Changes -* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129)) +* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands (transaction) ([#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) +* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129), [#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 563db515ca..f052cdc369 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -277,7 +277,7 @@ export type GlideString = string | Buffer; /** * Enum representing the different types of decoders. */ -export const enum Decoder { +export enum Decoder { /** * Decodes the response into a buffer array. */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 993cdf703b..212d40f670 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -27,6 +27,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, // eslint-disable-line @typescript-eslint/no-unused-vars + FunctionRestorePolicy, FunctionStatsSingleResponse, // eslint-disable-line @typescript-eslint/no-unused-vars GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -97,9 +98,11 @@ import { createFlushAll, createFlushDB, createFunctionDelete, + createFunctionDump, createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionRestore, createFunctionStats, createGeoAdd, createGeoDist, @@ -3233,6 +3236,34 @@ export class BaseTransaction> { return this.addAndReturn(createFunctionStats()); } + /** + * Returns the serialized payload of all loaded libraries. + * + * @see {@link https://valkey.io/commands/function-dump/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * @remarks To execute a transaction with a `functionDump` command, the `exec` command requires `Decoder.Bytes` to handle the response. + * + * Command Response - The serialized payload of all loaded libraries. + */ + public functionDump(): T { + return this.addAndReturn(createFunctionDump()); + } + + /** + * Restores libraries from the serialized payload returned by {@link functionDump}. + * + * @see {@link https://valkey.io/commands/function-restore/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param payload - The serialized data from {@link functionDump}. + * @param policy - (Optional) A policy for handling existing libraries. + * + * Command Response - `"OK"`. + */ + public functionRestore(payload: Buffer, policy?: FunctionRestorePolicy): T { + return this.addAndReturn(createFunctionRestore(payload, policy)); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 6c93134fae..5f1ccec953 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -1095,6 +1095,46 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function dump function restore in transaction %p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = await GlideClient.createClient(config); + expect(await client.functionFlush()).toEqual("OK"); + + try { + const name1 = "Foster"; + const name2 = "Dogster"; + // function returns first argument + const code = generateLuaLibCode( + name1, + new Map([[name2, "return args[1]"]]), + false, + ); + expect(await client.functionLoad(code)).toEqual(name1); + + // Verify functionDump + let transaction = new Transaction().functionDump(); + const result = await client.exec(transaction, Decoder.Bytes); + const data = result?.[0] as Buffer; + + // Verify functionRestore + transaction = new Transaction() + .functionRestore(data, FunctionRestorePolicy.REPLACE) + .fcall(name2, [], ["meow"]); + expect(await client.exec(transaction)).toEqual(["OK", "meow"]); + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", async (protocol) => { diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 8cd18e943a..88c2af5076 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -25,6 +25,7 @@ import { ReturnType, Routes, ScoreFilter, + SlotKeyTypes, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { @@ -1530,6 +1531,53 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + it("function dump function restore in transaction", async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = await GlideClusterClient.createClient(config); + const route: SlotKeyTypes = { + key: uuidv4(), + type: "primarySlotKey", + }; + expect(await client.functionFlush()).toEqual("OK"); + + try { + const name1 = "Foster"; + const name2 = "Dogster"; + // function returns first argument + const code = generateLuaLibCode( + name1, + new Map([[name2, "return args[1]"]]), + false, + ); + expect( + await client.functionLoad(code, true, route), + ).toEqual(name1); + + // Verify functionDump + let transaction = new ClusterTransaction().functionDump(); + const result = await client.exec(transaction, { + decoder: Decoder.Bytes, + route: route, + }); + const data = result?.[0] as Buffer; + + // Verify functionRestore + transaction = new ClusterTransaction() + .functionRestore(data, FunctionRestorePolicy.REPLACE) + .fcall(name2, [], ["meow"]); + expect( + await client.exec(transaction, { route: route }), + ).toEqual(["OK", "meow"]); + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }); }, ); From 2683edc3ffbd2a07e6a82c00ab1742ee01bd858b Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Wed, 21 Aug 2024 13:28:07 +0300 Subject: [PATCH 203/236] Node: Add binary support to LINDEX LMOVE BLMOVE LINSERT LLEN (#2166) * add binary support to lindex lmove blmove linsert llen --------- Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 40 ++++++++++----- node/src/Commands.ts | 18 +++---- node/tests/SharedTests.ts | 100 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 21 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index f052cdc369..5848362b2c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2189,7 +2189,7 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that there are 3 elements in the list. * ``` */ - public async llen(key: string): Promise { + public async llen(key: GlideString): Promise { return this.createWritePromise(createLLen(key)); } @@ -2205,6 +2205,8 @@ export class BaseClient { * @param destination - The key to the destination list. * @param whereFrom - The {@link ListDirection} to remove the element from. * @param whereTo - The {@link ListDirection} to add the element to. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The popped element, or `null` if `source` does not exist. * * @example @@ -2223,13 +2225,15 @@ export class BaseClient { * ``` */ public async lmove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, - ): Promise { + decoder?: Decoder, + ): Promise { return this.createWritePromise( createLMove(source, destination, whereFrom, whereTo), + { decoder: decoder }, ); } @@ -2249,6 +2253,8 @@ export class BaseClient { * @param whereFrom - The {@link ListDirection} to remove the element from. * @param whereTo - The {@link ListDirection} to add the element to. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The popped element, or `null` if `source` does not exist or if the operation timed-out. * * @example @@ -2266,14 +2272,16 @@ export class BaseClient { * ``` */ public async blmove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, timeout: number, - ): Promise { + decoder?: Decoder, + ): Promise { return this.createWritePromise( createBLMove(source, destination, whereFrom, whereTo, timeout), + { decoder: decoder }, ); } @@ -5258,6 +5266,8 @@ export class BaseClient { * * @param key - The `key` of the list. * @param index - The `index` of the element in the list to retrieve. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns - The element at `index` in the list stored at `key`. * If `index` is out of range or if `key` does not exist, null is returned. * @@ -5275,8 +5285,14 @@ export class BaseClient { * console.log(result); // Output: 'value3' - Returns the last element in the list stored at 'my_list'. * ``` */ - public async lindex(key: string, index: number): Promise { - return this.createWritePromise(createLIndex(key, index)); + public async lindex( + key: GlideString, + index: number, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createLIndex(key, index), { + decoder: decoder, + }); } /** @@ -5300,10 +5316,10 @@ export class BaseClient { * ``` */ public async linsert( - key: string, + key: GlideString, position: InsertPosition, - pivot: string, - element: string, + pivot: GlideString, + element: GlideString, ): Promise { return this.createWritePromise( createLInsert(key, position, pivot, element), diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 09bb359ecf..73255ef14f 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -884,7 +884,7 @@ export function createLRange( /** * @internal */ -export function createLLen(key: string): command_request.Command { +export function createLLen(key: GlideString): command_request.Command { return createCommand(RequestType.LLen, [key]); } @@ -906,8 +906,8 @@ export enum ListDirection { * @internal */ export function createLMove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, ): command_request.Command { @@ -923,8 +923,8 @@ export function createLMove( * @internal */ export function createBLMove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, timeout: number, @@ -1857,7 +1857,7 @@ export function createStrlen(key: string): command_request.Command { * @internal */ export function createLIndex( - key: string, + key: GlideString, index: number, ): command_request.Command { return createCommand(RequestType.LIndex, [key, index.toString()]); @@ -1881,10 +1881,10 @@ export enum InsertPosition { * @internal */ export function createLInsert( - key: string, + key: GlideString, position: InsertPosition, - pivot: string, - element: string, + pivot: GlideString, + element: GlideString, ): command_request.Command { return createCommand(RequestType.LInsert, [key, position, pivot, element]); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 19a8f09493..8230b483f2 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2081,6 +2081,7 @@ export function runBaseTests(config: { const valueList = ["value4", "value3", "value2", "value1"]; expect(await client.lpush(key1, valueList)).toEqual(4); expect(await client.llen(key1)).toEqual(4); + expect(await client.llen(Buffer.from(key1))).toEqual(4); expect(await client.llen("nonExistingKey")).toEqual(0); @@ -2108,12 +2109,16 @@ export function runBaseTests(config: { const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); + const key1Encoded = Buffer.from("{key}-1" + uuidv4()); + const key2Encoded = Buffer.from("{key}-2" + uuidv4()); const lpushArgs1 = ["2", "1"]; const lpushArgs2 = ["4", "3"]; // Initialize the tests expect(await client.lpush(key1, lpushArgs1)).toEqual(2); expect(await client.lpush(key2, lpushArgs2)).toEqual(2); + expect(await client.lpush(key1Encoded, lpushArgs1)).toEqual(2); + expect(await client.lpush(key2Encoded, lpushArgs2)).toEqual(2); // Move from LEFT to LEFT expect( @@ -2166,6 +2171,27 @@ export function runBaseTests(config: { expect(await client.lrange(key2, 0, -1)).toEqual(["1", "3"]); expect(await client.lrange(key1, 0, -1)).toEqual(["2", "4"]); + // Move from RIGHT to LEFT with encoded return value + expect( + await client.lmove( + key1, + key2, + ListDirection.RIGHT, + ListDirection.LEFT, + Decoder.Bytes, + ), + ).toEqual(Buffer.from("4")); + + // Move from RIGHT to LEFT with encoded list keys + expect( + await client.lmove( + key1Encoded, + key2Encoded, + ListDirection.RIGHT, + ListDirection.LEFT, + ), + ).toEqual("2"); + // Non-existing source key expect( await client.lmove( @@ -2212,12 +2238,16 @@ export function runBaseTests(config: { const key1 = "{key}-1" + uuidv4(); const key2 = "{key}-2" + uuidv4(); + const key1Encoded = Buffer.from("{key}-1" + uuidv4()); + const key2Encoded = Buffer.from("{key}-2" + uuidv4()); const lpushArgs1 = ["2", "1"]; const lpushArgs2 = ["4", "3"]; // Initialize the tests expect(await client.lpush(key1, lpushArgs1)).toEqual(2); expect(await client.lpush(key2, lpushArgs2)).toEqual(2); + expect(await client.lpush(key1Encoded, lpushArgs1)).toEqual(2); + expect(await client.lpush(key2Encoded, lpushArgs2)).toEqual(2); // Move from LEFT to LEFT with blocking expect( @@ -2281,6 +2311,29 @@ export function runBaseTests(config: { expect(await client.lrange(key2, 0, -1)).toEqual(["1", "3"]); expect(await client.lrange(key1, 0, -1)).toEqual(["2", "4"]); + // Move from RIGHT to LEFT with blocking and encoded return value + expect( + await client.blmove( + key1, + key2, + ListDirection.RIGHT, + ListDirection.LEFT, + 0.1, + Decoder.Bytes, + ), + ).toEqual(Buffer.from("4")); + + // Move from RIGHT to LEFT with encoded list keys + expect( + await client.blmove( + key1Encoded, + key2Encoded, + ListDirection.RIGHT, + ListDirection.LEFT, + 0.1, + ), + ).toEqual("2"); + // Non-existing source key with blocking expect( await client.blmove( @@ -5291,6 +5344,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const listName = uuidv4(); + const encodedListName = Buffer.from(uuidv4()); const listKey1Value = uuidv4(); const listKey2Value = uuidv4(); expect( @@ -5299,10 +5353,25 @@ export function runBaseTests(config: { listKey2Value, ]), ).toEqual(2); + expect( + await client.lpush(encodedListName, [ + Buffer.from(listKey1Value), + Buffer.from(listKey2Value), + ]), + ).toEqual(2); expect(await client.lindex(listName, 0)).toEqual(listKey2Value); expect(await client.lindex(listName, 1)).toEqual(listKey1Value); expect(await client.lindex("notExsitingList", 1)).toEqual(null); expect(await client.lindex(listName, 3)).toEqual(null); + expect(await client.lindex(listName, 0, Decoder.Bytes)).toEqual( + Buffer.from(listKey2Value), + ); + expect(await client.lindex(listName, 1, Decoder.Bytes)).toEqual( + Buffer.from(listKey1Value), + ); + expect(await client.lindex(encodedListName, 0)).toEqual( + listKey2Value, + ); }, protocol); }, config.timeout, @@ -5313,6 +5382,8 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); + const key2 = uuidv4(); + const key2Encoded = Buffer.from(key2); const stringKey = uuidv4(); const nonExistingKey = uuidv4(); @@ -5361,6 +5432,35 @@ export function runBaseTests(config: { ), ).toEqual(0); + // key, pivot and element as buffers + expect(await client.lpush(key2, ["4", "3", "2", "1"])).toEqual( + 4, + ); + expect( + await client.linsert( + key2Encoded, + InsertPosition.Before, + Buffer.from("2"), + Buffer.from("1.5"), + ), + ).toEqual(5); + expect( + await client.linsert( + key2Encoded, + InsertPosition.After, + Buffer.from("3"), + Buffer.from("3.5"), + ), + ).toEqual(6); + expect(await client.lrange(key2Encoded, 0, -1)).toEqual([ + "1", + "1.5", + "2", + "3", + "3.5", + "4", + ]); + // key exists, but it is not a list expect(await client.set(stringKey, "value")).toEqual("OK"); await expect( From 908032e550370f3aa22e4531834f57f8abe61bd5 Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Wed, 21 Aug 2024 14:13:18 +0300 Subject: [PATCH 204/236] Node: Add binary support to RPUSH RPOP RPOPCOUNT BLPOP BRPOP (#2153) * add binary support to rpush rpop rpopcount blpop brpop * updated decoder docs for all functions where binary variant was added --------- Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 65 ++++++++++++++++++++++++---------- node/src/Commands.ts | 13 +++---- node/src/GlideClient.ts | 3 +- node/src/GlideClusterClient.ts | 3 +- node/tests/SharedTests.ts | 35 +++++++++++++++--- 5 files changed, 88 insertions(+), 31 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5848362b2c..d6b409694c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -937,7 +937,8 @@ export class BaseClient { * @see {@link https://valkey.io/commands/get/|valkey.io} for details. * * @param key - The key to retrieve from the database. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns If `key` exists, returns the value of `key`. Otherwise, return null. * * @example @@ -988,7 +989,8 @@ export class BaseClient { * @see {@link https://valkey.io/commands/getdel/|valkey.io} for details. * * @param key - The key to retrieve from the database. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns If `key` exists, returns the `value` of `key`. Otherwise, return `null`. * * @example @@ -1018,7 +1020,8 @@ export class BaseClient { * @param key - The key of the string. * @param start - The starting offset. * @param end - The ending offset. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A substring extracted from the value stored at `key`. * * @example @@ -1190,7 +1193,8 @@ export class BaseClient { * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. * * @param keys - A list of keys to retrieve values for. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of values corresponding to the provided keys. If a key is not found, * its corresponding value in the list will be null. * @@ -1578,7 +1582,8 @@ export class BaseClient { * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns the value associated with `field`, or null when `field` is not present in the hash or `key` does not exist. * * @example @@ -1854,7 +1859,8 @@ export class BaseClient { * @see {@link https://valkey.io/commands/hvals/|valkey.io} for more details. * * @param key - The key of the hash. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns a list of values in the hash, or an empty list when the key does not exist. * * @example @@ -2389,7 +2395,10 @@ export class BaseClient { * console.log(result); // Output: 1 * ``` */ - public async rpush(key: string, elements: string[]): Promise { + public async rpush( + key: GlideString, + elements: GlideString[], + ): Promise { return this.createWritePromise(createRPush(key, elements)); } @@ -2418,6 +2427,8 @@ export class BaseClient { * @see {@link https://valkey.io/commands/rpop/|valkey.io} for details. * * @param key - The key of the list. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The value of the last element. * If `key` does not exist null will be returned. * @@ -2435,8 +2446,11 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public async rpop(key: string): Promise { - return this.createWritePromise(createRPop(key)); + public async rpop( + key: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createRPop(key), { decoder: decoder }); } /** Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. @@ -2445,6 +2459,8 @@ export class BaseClient { * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of popped elements will be returned depending on the list's length. * If `key` does not exist null will be returned. * @@ -2463,10 +2479,13 @@ export class BaseClient { * ``` */ public async rpopCount( - key: string, + key: GlideString, count: number, - ): Promise { - return this.createWritePromise(createRPop(key, count)); + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createRPop(key, count), { + decoder: decoder, + }); } /** Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. @@ -5402,6 +5421,8 @@ export class BaseClient { * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. * @@ -5413,10 +5434,13 @@ export class BaseClient { * ``` */ public async brpop( - keys: string[], + keys: GlideString[], timeout: number, - ): Promise<[string, string] | null> { - return this.createWritePromise(createBRPop(keys, timeout)); + decoder?: Decoder, + ): Promise<[GlideString, GlideString] | null> { + return this.createWritePromise(createBRPop(keys, timeout), { + decoder: decoder, + }); } /** Blocking list pop primitive. @@ -5430,6 +5454,8 @@ export class BaseClient { * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. * @@ -5440,10 +5466,13 @@ export class BaseClient { * ``` */ public async blpop( - keys: string[], + keys: GlideString[], timeout: number, - ): Promise<[string, string] | null> { - return this.createWritePromise(createBLPop(keys, timeout)); + decoder?: Decoder, + ): Promise<[GlideString, GlideString] | null> { + return this.createWritePromise(createBLPop(keys, timeout), { + decoder: decoder, + }); } /** Adds all elements to the HyperLogLog data structure stored at the specified `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 73255ef14f..97056f5e6c 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -979,8 +979,8 @@ export function createLRem( * @internal */ export function createRPush( - key: string, - elements: string[], + key: GlideString, + elements: GlideString[], ): command_request.Command { return createCommand(RequestType.RPush, [key].concat(elements)); } @@ -999,10 +999,11 @@ export function createRPushX( * @internal */ export function createRPop( - key: string, + key: GlideString, count?: number, ): command_request.Command { - const args: string[] = count == undefined ? [key] : [key, count.toString()]; + const args: GlideString[] = + count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.RPop, args); } @@ -2215,7 +2216,7 @@ export function createPublish( * @internal */ export function createBRPop( - keys: string[], + keys: GlideString[], timeout: number, ): command_request.Command { const args = [...keys, timeout.toString()]; @@ -2226,7 +2227,7 @@ export function createBRPop( * @internal */ export function createBLPop( - keys: string[], + keys: GlideString[], timeout: number, ): command_request.Command { const args = [...keys, timeout.toString()]; diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index d542a4057d..053e3ddb16 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -229,7 +229,8 @@ export class GlideClient extends BaseClient { * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". * If provided, the server will respond with a copy of the message. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. * * @example diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c9dc39f067..da299ec5a4 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -415,7 +415,8 @@ export class GlideClusterClient extends BaseClient { * If provided, the server will respond with a copy of the message. * @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`. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. * * @example diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 8230b483f2..fbb2e9af3a 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2494,15 +2494,32 @@ export function runBaseTests(config: { `rpush and rpop with existing and non existing key_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); - const valueList = ["value1", "value2", "value3", "value4"]; - expect(await client.rpush(key, valueList)).toEqual(4); - expect(await client.rpop(key)).toEqual("value4"); - expect(await client.rpopCount(key, 2)).toEqual([ + const key1 = uuidv4(); + const key2 = Buffer.from(uuidv4()); + const valueList1 = ["value1", "value2", "value3", "value4"]; + const valueList2 = ["value5", "value6", "value7"]; + expect(await client.rpush(key1, valueList1)).toEqual(4); + expect(await client.rpop(key1)).toEqual("value4"); + expect(await client.rpopCount(key1, 2)).toEqual([ "value3", "value2", ]); expect(await client.rpop("nonExistingKey")).toEqual(null); + + expect(await client.rpush(key2, valueList2)).toEqual(3); + expect(await client.rpop(key2, Decoder.Bytes)).toEqual( + Buffer.from("value7"), + ); + expect(await client.rpopCount(key2, 2, Decoder.Bytes)).toEqual([ + Buffer.from("value6"), + Buffer.from("value5"), + ]); + expect( + await client.rpush(key2, [Buffer.from("value8")]), + ).toEqual(1); + expect(await client.rpop(key2, Decoder.Bytes)).toEqual( + Buffer.from("value8"), + ); }, protocol); }, config.timeout, @@ -5750,6 +5767,10 @@ export function runBaseTests(config: { "brpop-test", "baz", ]); + // Test encoded value + expect( + await client.brpop(["brpop-test"], 0.1, Decoder.Bytes), + ).toEqual([Buffer.from("brpop-test"), Buffer.from("bar")]); // Delete all values from list expect(await client.del(["brpop-test"])).toEqual(1); // Test null return when key doesn't exist @@ -5787,6 +5808,10 @@ export function runBaseTests(config: { "blpop-test", "foo", ]); + // Test decoded value + expect( + await client.blpop(["blpop-test"], 0.1, Decoder.Bytes), + ).toEqual([Buffer.from("blpop-test"), Buffer.from("bar")]); // Delete all values from list expect(await client.del(["blpop-test"])).toEqual(1); // Test null return when key doesn't exist From 29a69205db593fcc9d30737255b765b446685bee Mon Sep 17 00:00:00 2001 From: ort-bot Date: Thu, 22 Aug 2024 00:22:07 +0000 Subject: [PATCH 205/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 530 ++++++++++++++++++++++++++- java/THIRD_PARTY_LICENSES_JAVA | 530 ++++++++++++++++++++++++++- node/THIRD_PARTY_LICENSES_NODE | 524 +++++++++++++++++++++++++- python/THIRD_PARTY_LICENSES_PYTHON | 530 ++++++++++++++++++++++++++- 4 files changed, 2089 insertions(+), 25 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 2b6d8abbb1..d524c5e3f5 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -683,6 +683,248 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: adler2:2.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -5152,13 +5394,38 @@ The following copyrights and licenses were found in the source code of this pack http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dashmap:6.0.1 - -- +The following copyrights and licenses were found in the source code of this package: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -6809,7 +7076,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.31 +Package: flate2:1.0.32 The following copyrights and licenses were found in the source code of this package: @@ -13582,6 +13849,255 @@ the following restrictions: ---- +Package: miniz_oxide:0.8.0 + +The following copyrights and licenses were found in the source code of this package: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + Package: mio:1.0.2 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 6de08f9da2..c88d1c23d7 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -683,6 +683,248 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: adler2:2.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -5381,13 +5623,38 @@ The following copyrights and licenses were found in the source code of this pack http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dashmap:6.0.1 - -- +The following copyrights and licenses were found in the source code of this package: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -7038,7 +7305,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.31 +Package: flate2:1.0.32 The following copyrights and licenses were found in the source code of this package: @@ -14477,6 +14744,255 @@ the following restrictions: ---- +Package: miniz_oxide:0.8.0 + +The following copyrights and licenses were found in the source code of this package: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + Package: mio:1.0.2 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 cfc4bf93f2..2005cc23e4 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -683,6 +683,248 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: adler2:2.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -5487,6 +5729,31 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: dashmap:6.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: deranged:0.3.11 The following copyrights and licenses were found in the source code of this package: @@ -7115,7 +7382,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.31 +Package: flate2:1.0.32 The following copyrights and licenses were found in the source code of this package: @@ -14114,6 +14381,255 @@ the following restrictions: ---- +Package: miniz_oxide:0.8.0 + +The following copyrights and licenses were found in the source code of this package: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -36093,7 +36609,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: is-core-module:2.15.0 +Package: is-core-module:2.15.1 The following copyrights and licenses were found in the source code of this package: @@ -36994,7 +37510,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobufjs:7.3.2 +Package: protobufjs:7.3.3 The following copyrights and licenses were found in the source code of this package: @@ -37903,7 +38419,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: @types:node:22.4.1 +Package: @types:node:22.5.0 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 552d6baf7c..7e07561a8c 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -683,6 +683,248 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: adler2:2.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -5152,13 +5394,38 @@ The following copyrights and licenses were found in the source code of this pack http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dashmap:6.0.1 - -- +The following copyrights and licenses were found in the source code of this package: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -6809,7 +7076,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.31 +Package: flate2:1.0.32 The following copyrights and licenses were found in the source code of this package: @@ -14273,6 +14540,255 @@ the following restrictions: ---- +Package: miniz_oxide:0.8.0 + +The following copyrights and licenses were found in the source code of this package: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + Package: mio:1.0.2 The following copyrights and licenses were found in the source code of this package: From 20f8868b57cdbf2eacd8af0c752c968828c875f6 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 22 Aug 2024 09:47:55 -0700 Subject: [PATCH 206/236] Node: add missing `async`s and exports (#2184) Fixes. Signed-off-by: Yury-Fridlyand --- node/npm/glide/index.ts | 16 ++++++++++++++-- node/src/BaseClient.ts | 14 +++++++------- node/src/Commands.ts | 21 +++++++++++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 733de07d6a..58882c3ffd 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -75,6 +75,7 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { + AggregationType, BaseScanOptions, BitEncoding, BitFieldGet, @@ -98,6 +99,7 @@ function initialize() { GeoCircleShape, GeoSearchShape, GeoSearchResultOptions, + GeoSearchStoreResultOptions, SortOrder, GeoUnit, GeospatialData, @@ -120,6 +122,8 @@ function initialize() { PeriodicChecksManualInterval, PeriodicChecks, Logger, + Limit, + LolwutOptions, LPosOptions, ListDirection, ExpireOptions, @@ -127,8 +131,9 @@ function initialize() { InfoOptions, InsertPosition, SetOptions, - ZaddOptions, + ZAddOptions, InfBoundary, + KeyWeight, Boundary, UpdateOptions, ProtocolVersion, @@ -164,6 +169,7 @@ function initialize() { ScoreFilter, SignedEncoding, UnsignedEncoding, + UpdateByScore, createLeakedArray, createLeakedAttribute, createLeakedBigint, @@ -174,6 +180,7 @@ function initialize() { } = nativeBinding; module.exports = { + AggregationType, BaseScanOptions, BitEncoding, BitFieldGet, @@ -198,6 +205,7 @@ function initialize() { GeoCircleShape, GeoSearchShape, GeoSearchResultOptions, + GeoSearchStoreResultOptions, SortOrder, GeoUnit, GeospatialData, @@ -221,6 +229,8 @@ function initialize() { PeriodicChecksManualInterval, PeriodicChecks, Logger, + LolwutOptions, + Limit, LPosOptions, ListDirection, ExpireOptions, @@ -228,8 +238,9 @@ function initialize() { InfoOptions, InsertPosition, SetOptions, - ZaddOptions, + ZAddOptions, InfBoundary, + KeyWeight, Boundary, UpdateOptions, ProtocolVersion, @@ -263,6 +274,7 @@ function initialize() { ScoreFilter, SignedEncoding, UnsignedEncoding, + UpdateByScore, createLeakedArray, createLeakedAttribute, createLeakedBigint, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d6b409694c..9ece045d89 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -180,6 +180,7 @@ import { createXGroupCreateConsumer, createXGroupDelConsumer, createXGroupDestroy, + createXGroupSetid, createXInfoConsumers, createXInfoGroups, createXInfoStream, @@ -220,7 +221,6 @@ import { createZScore, createZUnion, createZUnionStore, - createXGroupSetid, } from "./Commands"; import { ClosingError, @@ -1650,7 +1650,7 @@ export class BaseClient { * console.log(result); // Output: ["field1", "field2", "field3"] - Returns all the field names stored in the hash "my_hash". * ``` */ - public hkeys(key: string): Promise { + public async hkeys(key: string): Promise { return this.createWritePromise(createHKeys(key)); } @@ -3913,7 +3913,7 @@ export class BaseClient { * console.log(result); // Output: ['member1'] * ``` */ - public zinter(keys: string[]): Promise { + public async zinter(keys: string[]): Promise { return this.createWritePromise(createZInter(keys)); } @@ -3945,7 +3945,7 @@ export class BaseClient { * console.log(result2); // Output: {'member1': 10.5} - "member1" with score of 10.5 is the result. * ``` */ - public zinterWithScores( + public async zinterWithScores( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): Promise> { @@ -3977,7 +3977,7 @@ export class BaseClient { * console.log(result); // Output: ['member1', 'member2'] * ``` */ - public zunion(keys: string[]): Promise { + public async zunion(keys: string[]): Promise { return this.createWritePromise(createZUnion(keys)); } @@ -4008,7 +4008,7 @@ export class BaseClient { * console.log(result2); // {'member1': 10.5, 'member2': 8.2} * ``` */ - public zunionWithScores( + public async zunionWithScores( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): Promise> { @@ -4668,7 +4668,7 @@ export class BaseClient { * // } * ``` */ - public xreadgroup( + public async xreadgroup( group: string, consumer: string, keys_and_ids: Record, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 97056f5e6c..e9aa5bb65d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1986,6 +1986,7 @@ export function createZLexCount( return createCommand(RequestType.ZLexCount, args); } +/** @internal */ export function createZRank( key: string, member: string, @@ -3356,13 +3357,25 @@ function convertGeoSearchOptionsToArgs( } if (resultOptions) { - if ("withCoord" in resultOptions && resultOptions.withCoord) + if ( + "withCoord" in resultOptions && + (resultOptions as GeoSearchResultOptions).withCoord + ) args.push("WITHCOORD"); - if ("withDist" in resultOptions && resultOptions.withDist) + if ( + "withDist" in resultOptions && + (resultOptions as GeoSearchResultOptions).withDist + ) args.push("WITHDIST"); - if ("withHash" in resultOptions && resultOptions.withHash) + if ( + "withHash" in resultOptions && + (resultOptions as GeoSearchResultOptions).withHash + ) args.push("WITHHASH"); - if ("storeDist" in resultOptions && resultOptions.storeDist) + if ( + "storeDist" in resultOptions && + (resultOptions as GeoSearchStoreResultOptions).storeDist + ) args.push("STOREDIST"); if (resultOptions.count) { From 99f9e883b5b0975ea7d179f76f5c84bd4e2bd2a7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 22 Aug 2024 10:02:22 -0700 Subject: [PATCH 207/236] Node: Update tests for blocking commands. (#2127) * Update tests for blocking commands. Signed-off-by: Yury-Fridlyand --- node/src/Commands.ts | 4 +- node/tests/GlideClient.test.ts | 41 ------------------ node/tests/SharedTests.ts | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e9aa5bb65d..eadbe48175 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2497,8 +2497,8 @@ export enum FlushMode { export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or - * until the server has the required number of entries. Equivalent to `BLOCK` - * in the Redis API. + * until the server has the required number of entries. A value of `0` will block indefinitely. + * Equivalent to `BLOCK` in the Redis API. */ block?: number; /** diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 5f1ccec953..5167307d07 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -15,7 +15,6 @@ import { v4 as uuidv4 } from "uuid"; import { Decoder, GlideClient, - ListDirection, ProtocolVersion, RequestError, Transaction, @@ -130,46 +129,6 @@ describe("GlideClient", () => { }, ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "check that blocking commands returns never timeout_%p", - async (protocol) => { - client = await GlideClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol, { - requestTimeout: 300, - }), - ); - - const promiseList = [ - client.blmove( - "source", - "destination", - ListDirection.LEFT, - ListDirection.LEFT, - 0.1, - ), - client.blmpop(["key1", "key2"], ListDirection.LEFT, 0.1), - client.bzpopmax(["key1", "key2"], 0), - client.bzpopmin(["key1", "key2"], 0), - ]; - - try { - for (const promise of promiseList) { - const timeoutPromise = new Promise((resolve) => { - setTimeout(resolve, 500); - }); - await Promise.race([promise, timeoutPromise]); - } - } finally { - for (const promise of promiseList) { - await Promise.resolve([promise]); - } - - client.close(); - } - }, - 5000, - ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "select dbsize flushdb test %p", async (protocol) => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index fbb2e9af3a..b157452d04 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -36,6 +36,7 @@ import { ListDirection, ProtocolVersion, RequestError, + ReturnType, ScoreFilter, Script, SignedEncoding, @@ -10476,6 +10477,82 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "check that blocking commands never time out %p", + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key1 = "{blocking}-1-" + uuidv4(); + const key2 = "{blocking}-2-" + uuidv4(); + const key3 = "{blocking}-3-" + uuidv4(); // stream + const keyz = [key1, key2]; + + // create a group and a stream, so `xreadgroup` won't fail on missing group + await client.xgroupCreate(key3, "group", "0", { + mkStream: true, + }); + + const promiseList: [string, Promise][] = [ + ["bzpopmax", client.bzpopmax(keyz, 0)], + ["bzpopmin", client.bzpopmin(keyz, 0)], + ["blpop", client.blpop(keyz, 0)], + ["brpop", client.brpop(keyz, 0)], + ["xread", client.xread({ [key3]: "0-0" }, { block: 0 })], + [ + "xreadgroup", + client.xreadgroup( + "group", + "consumer", + { [key3]: "0-0" }, + { block: 0 }, + ), + ], + ["wait", client.wait(42, 0)], + ]; + + if (!cluster.checkIfServerVersionLessThan("6.2.0")) { + promiseList.push([ + "blmove", + client.blmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.LEFT, + 0, + ), + ]); + } + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + promiseList.push( + ["blmpop", client.blmpop(keyz, ListDirection.LEFT, 0)], + ["bzmpop", client.bzmpop(keyz, ScoreFilter.MAX, 0)], + ); + } + + try { + for (const [name, promise] of promiseList) { + const timeoutPromise = new Promise((resolve) => { + setTimeout(resolve, 500, "timeOutPromiseWins"); + }); + // client has default request timeout 250 ms, we run all commands with infinite blocking + // we expect that all commands will still await for the response even after 500 ms + expect( + await Promise.race([ + promise.finally(() => + fail(`${name} didn't block infintely`), + ), + timeoutPromise, + ]), + ).toEqual("timeOutPromiseWins"); + } + } finally { + client.close(); + } + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `getex test_%p`, async (protocol) => { From 3307cd797bfdc9acdabb0d8b6156616984a4a20e Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Thu, 22 Aug 2024 10:34:35 -0700 Subject: [PATCH 208/236] Node: add binary variant to hyperloglog commands (#2176) * Node: add binary variant to hyperloglog commands --------- Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 15 ++++++++++----- node/src/Commands.ts | 10 +++++----- node/src/Transaction.ts | 8 ++++---- node/tests/SharedTests.ts | 14 ++++++++++---- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 730c391284..b6305dafe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to HYPERLOGLOG commands ([#2176](https://github.com/valkey-io/valkey-glide/pull/2176)) * Node: Added FUNCTION DUMP and FUNCTION RESTORE commands (transaction) ([#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) * Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129), [#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9ece045d89..a0894ea191 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -5493,7 +5493,10 @@ export class BaseClient { * console.log(result); // Output: 1 - Indicates that a new empty data structure was created * ``` */ - public async pfadd(key: string, elements: string[]): Promise { + public async pfadd( + key: GlideString, + elements: GlideString[], + ): Promise { return this.createWritePromise(createPfAdd(key, elements)); } @@ -5512,7 +5515,7 @@ export class BaseClient { * console.log(result); // Output: 4 - The approximated cardinality of the union of "hll_1" and "hll_2" * ``` */ - public async pfcount(keys: string[]): Promise { + public async pfcount(keys: GlideString[]): Promise { return this.createWritePromise(createPfCount(keys)); } @@ -5538,10 +5541,12 @@ export class BaseClient { * ``` */ public async pfmerge( - destination: string, - sourceKeys: string[], + destination: GlideString, + sourceKeys: GlideString[], ): Promise<"OK"> { - return this.createWritePromise(createPfMerge(destination, sourceKeys)); + return this.createWritePromise(createPfMerge(destination, sourceKeys), { + decoder: Decoder.String, + }); } /** Returns the internal encoding for the Valkey object stored at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index eadbe48175..3d65bf6cd6 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2826,8 +2826,8 @@ export function createRenameNX( * @internal */ export function createPfAdd( - key: string, - elements: string[], + key: GlideString, + elements: GlideString[], ): command_request.Command { const args = [key, ...elements]; return createCommand(RequestType.PfAdd, args); @@ -2836,7 +2836,7 @@ export function createPfAdd( /** * @internal */ -export function createPfCount(keys: string[]): command_request.Command { +export function createPfCount(keys: GlideString[]): command_request.Command { return createCommand(RequestType.PfCount, keys); } @@ -2844,8 +2844,8 @@ export function createPfCount(keys: string[]): command_request.Command { * @internal */ export function createPfMerge( - destination: string, - sourceKey: string[], + destination: GlideString, + sourceKey: GlideString[], ): command_request.Command { return createCommand(RequestType.PfMerge, [destination, ...sourceKey]); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 212d40f670..0ee31d892f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -219,6 +219,7 @@ import { createXGroupCreateConsumer, createXGroupDelConsumer, createXGroupDestroy, + createXGroupSetid, createXInfoConsumers, createXInfoGroups, createXInfoStream, @@ -259,7 +260,6 @@ import { createZScore, createZUnion, createZUnionStore, - createXGroupSetid, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -3017,7 +3017,7 @@ export class BaseTransaction> { * Command Response - If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is * altered, then returns `1`. Otherwise, returns `0`. */ - public pfadd(key: string, elements: string[]): T { + public pfadd(key: GlideString, elements: GlideString[]): T { return this.addAndReturn(createPfAdd(key, elements)); } @@ -3030,7 +3030,7 @@ export class BaseTransaction> { * Command Response - The approximated cardinality of given HyperLogLog data structures. * The cardinality of a key that does not exist is `0`. */ - public pfcount(keys: string[]): T { + public pfcount(keys: GlideString[]): T { return this.addAndReturn(createPfCount(keys)); } @@ -3044,7 +3044,7 @@ export class BaseTransaction> { * @param sourceKeys - The keys of the HyperLogLog structures to be merged. * Command Response - A simple "OK" response. */ - public pfmerge(destination: string, sourceKeys: string[]): T { + public pfmerge(destination: GlideString, sourceKeys: GlideString[]): T { return this.addAndReturn(createPfMerge(destination, sourceKeys)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b157452d04..d2b63bb502 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6976,7 +6976,9 @@ export function runBaseTests(config: { const key = uuidv4(); expect(await client.pfadd(key, [])).toEqual(1); expect(await client.pfadd(key, ["one", "two"])).toEqual(1); - expect(await client.pfadd(key, ["two"])).toEqual(0); + expect( + await client.pfadd(Buffer.from(key), [Buffer.from("two")]), + ).toEqual(0); expect(await client.pfadd(key, [])).toEqual(0); // key exists, but it is not a HyperLogLog @@ -7000,7 +7002,7 @@ export function runBaseTests(config: { expect(await client.pfadd(key1, ["a", "b", "c"])).toEqual(1); expect(await client.pfadd(key2, ["b", "c", "d"])).toEqual(1); expect(await client.pfcount([key1])).toEqual(3); - expect(await client.pfcount([key2])).toEqual(3); + expect(await client.pfcount([Buffer.from(key2)])).toEqual(3); expect(await client.pfcount([key1, key2])).toEqual(4); expect( await client.pfcount([key1, key2, nonExistingKey]), @@ -7041,11 +7043,15 @@ export function runBaseTests(config: { expect(await client.pfadd(key2, ["b", "c", "d"])).toEqual(1); // merge into new HyperLogLog data set - expect(await client.pfmerge(key3, [key1, key2])).toEqual("OK"); + expect( + await client.pfmerge(Buffer.from(key3), [key1, key2]), + ).toEqual("OK"); expect(await client.pfcount([key3])).toEqual(4); // merge into existing HyperLogLog data set - expect(await client.pfmerge(key1, [key2])).toEqual("OK"); + expect(await client.pfmerge(key1, [Buffer.from(key2)])).toEqual( + "OK", + ); expect(await client.pfcount([key1])).toEqual(4); // non-existing source key From 697bc78b1486d231890d3f6492d25f1962afd381 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 22 Aug 2024 12:14:45 -0700 Subject: [PATCH 209/236] Node: Added binary variant to geo commands. (#2149) * Added binary variant to geo commands. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 2 +- node/npm/glide/index.ts | 4 ++ node/src/BaseClient.ts | 69 ++++++++++++++----------- node/src/Commands.ts | 48 ++++++++++------- node/src/Transaction.ts | 20 ++++---- node/tests/SharedTests.ts | 105 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6305dafe4..4601ec6280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ #### Changes +* Node: Added binary variant to geo commands ([#2149](https://github.com/valkey-io/valkey-glide/pull/2149)) * Node: Added binary variant to HYPERLOGLOG commands ([#2176](https://github.com/valkey-io/valkey-glide/pull/2176)) -* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands (transaction) ([#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) * Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129), [#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 58882c3ffd..68efeadfda 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -91,6 +91,8 @@ function initialize() { BitmapIndexType, BitwiseOperation, ConditionalChange, + Decoder, + DecoderOption, GeoAddOptions, CoordOrigin, MemberOrigin, @@ -196,6 +198,8 @@ function initialize() { BitmapIndexType, BitwiseOperation, ConditionalChange, + Decoder, + DecoderOption, GeoAddOptions, GlideString, CoordOrigin, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a0894ea191..c9ff56dfb9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -24,6 +24,7 @@ import { BitwiseOperation, Boundary, CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars + DecoderOption, ExpireOptions, GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -5756,8 +5757,8 @@ export class BaseClient { * ``` */ public async geoadd( - key: string, - membersToGeospatialData: Map, + key: GlideString, + membersToGeospatialData: Map, options?: GeoAddOptions, ): Promise { return this.createWritePromise( @@ -5779,7 +5780,8 @@ export class BaseClient { * @param searchBy - The query's shape options, could be one of: * - {@link GeoCircleShape} to search inside circular area according to given radius. * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. - * @param resultOptions - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}. + * @param options - (Optional) Parameters to request additional information and configure sorting/limiting the results, + * see {@link GeoSearchResultOptions} and {@link DecoderOption}. * @returns By default, returns an `Array` of members (locations) names. * If any of `withCoord`, `withDist` or `withHash` are set to `true` in {@link GeoSearchResultOptions}, a 2D `Array` returned, * where each sub-array represents a single item in the following order: @@ -5790,7 +5792,7 @@ export class BaseClient { * * @example * ```typescript - * const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]); + * const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]); * await client.geoadd("mySortedSet", data); * // search for locations within 200 km circle around stored member named 'Palermo' * const result1 = await client.geosearch("mySortedSet", { member: "Palermo" }, { radius: 200, unit: GeoUnit.KILOMETERS }); @@ -5832,13 +5834,14 @@ export class BaseClient { * ``` */ public async geosearch( - key: string, + key: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, - resultOptions?: GeoSearchResultOptions, - ): Promise<(string | (number | number[])[])[]> { + options?: GeoSearchResultOptions & DecoderOption, + ): Promise<[GlideString, [number?, number?, [number, number]?]?][]> { return this.createWritePromise( - createGeoSearch(key, searchFrom, searchBy, resultOptions), + createGeoSearch(key, searchFrom, searchBy, options), + { decoder: options?.decoder }, ); } @@ -5862,7 +5865,8 @@ export class BaseClient { * @param searchBy - The query's shape options, could be one of: * - {@link GeoCircleShape} to search inside circular area according to given radius. * - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width. - * @param resultOptions - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchStoreResultOptions}. + * @param options - (Optional) Parameters to request additional information and configure sorting/limiting the results, + * see {@link GeoSearchStoreResultOptions}. * @returns The number of elements in the resulting sorted set stored at `destination`. * * @example @@ -5902,11 +5906,11 @@ export class BaseClient { * ``` */ public async geosearchstore( - destination: string, - source: string, + destination: GlideString, + source: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, - resultOptions?: GeoSearchStoreResultOptions, + options?: GeoSearchStoreResultOptions, ): Promise { return this.createWritePromise( createGeoSearchStore( @@ -5914,7 +5918,7 @@ export class BaseClient { source, searchFrom, searchBy, - resultOptions, + options, ), ); } @@ -5938,13 +5942,18 @@ export class BaseClient { * const result = await client.geopos("mySortedSet", ["Palermo", "Catania", "NonExisting"]); * // When added via GEOADD, the geospatial coordinates are converted into a 52 bit geohash, so the coordinates * // returned might not be exactly the same as the input values - * console.log(result); // Output: [[13.36138933897018433, 38.11555639549629859], [15.08726745843887329, 37.50266842333162032], null] + * console.log(result); // Output: + * // [ + * // [13.36138933897018433, 38.11555639549629859], + * // [15.08726745843887329, 37.50266842333162032], + * // null + * // ] * ``` */ public async geopos( - key: string, - members: string[], - ): Promise<(number[] | null)[]> { + key: GlideString, + members: GlideString[], + ): Promise<([number, number] | null)[]> { return this.createWritePromise(createGeoPos(key, members)); } @@ -6107,7 +6116,7 @@ export class BaseClient { * @param key - The key of the sorted set. * @param member1 - The name of the first member. * @param member2 - The name of the second member. - * @param geoUnit - The unit of distance measurement - see {@link GeoUnit}. If not specified, the default unit is {@link GeoUnit.METERS}. + * @param geoUnit - (Optional) The unit of distance measurement - see {@link GeoUnit}. If not specified, the {@link GeoUnit.METERS} is used as a default unit. * @returns The distance between `member1` and `member2`. Returns `null`, if one or both members do not exist, * or if the key does not exist. * @@ -6118,9 +6127,9 @@ export class BaseClient { * ``` */ public async geodist( - key: string, - member1: string, - member2: string, + key: GlideString, + member1: GlideString, + member2: GlideString, geoUnit?: GeoUnit, ): Promise { return this.createWritePromise( @@ -6136,23 +6145,21 @@ export class BaseClient { * @param key - The key of the sorted set. * @param members - The array of members whose `GeoHash` strings are to be retrieved. * @returns An array of `GeoHash` strings representing the positions of the specified members stored at `key`. - * If a member does not exist in the sorted set, a `null` value is returned for that member. + * If a member does not exist in the sorted set, a `null` value is returned for that member. * * @example * ```typescript - * const result = await client.geohash("mySortedSet",["Palermo", "Catania", "NonExisting"]); - * console.log(num); // Output: ["sqc8b49rny0", "sqdtr74hyu0", null] + * const result = await client.geohash("mySortedSet", ["Palermo", "Catania", "NonExisting"]); + * console.log(result); // Output: ["sqc8b49rny0", "sqdtr74hyu0", null] * ``` */ public async geohash( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): Promise<(string | null)[]> { - return this.createWritePromise<(string | null)[]>( - createGeoHash(key, members), - ).then((hashes) => - hashes.map((hash) => (hash === null ? null : "" + hash)), - ); + return this.createWritePromise(createGeoHash(key, members), { + decoder: Decoder.String, + }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 3d65bf6cd6..b4a1b88fae 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -89,6 +89,15 @@ function createCommand( return singleCommand; } +/** An extension to command option types. */ +export type DecoderOption = { + /** + * {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. + */ + decoder?: Decoder; +}; + /** * @internal */ @@ -3095,7 +3104,8 @@ export function createDBSize(): command_request.Command { } /** - * An optional condition to the {@link BaseClient.geoadd} command. + * An optional condition to the {@link BaseClient.geoadd | geoadd}, + * {@link BaseClient.zadd | zadd} and {@link BaseClient.set | set} commands. */ export enum ConditionalChange { /** @@ -3140,11 +3150,11 @@ export type GeoAddOptions = { * @internal */ export function createGeoAdd( - key: string, - membersToGeospatialData: Map, + key: GlideString, + membersToGeospatialData: Map, options?: GeoAddOptions, ): command_request.Command { - let args: string[] = [key]; + let args: GlideString[] = [key]; if (options) { if (options.updateMode) { @@ -3182,8 +3192,8 @@ export enum GeoUnit { * @internal */ export function createGeoPos( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): command_request.Command { return createCommand(RequestType.GeoPos, [key].concat(members)); } @@ -3192,12 +3202,12 @@ export function createGeoPos( * @internal */ export function createGeoDist( - key: string, - member1: string, - member2: string, + key: GlideString, + member1: GlideString, + member2: GlideString, geoUnit?: GeoUnit, ): command_request.Command { - const args: string[] = [key, member1, member2]; + const args = [key, member1, member2]; if (geoUnit) { args.push(geoUnit); @@ -3210,10 +3220,10 @@ export function createGeoDist( * @internal */ export function createGeoHash( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): command_request.Command { - const args: string[] = [key].concat(members); + const args = [key].concat(members); return createCommand(RequestType.GeoHash, args); } @@ -3294,12 +3304,12 @@ export type CoordOrigin = { /** The search origin represented by an existing member. */ export type MemberOrigin = { /** Member (location) name stored in the sorted set to use as a search pivot. */ - member: string; + member: GlideString; }; /** @internal */ export function createGeoSearch( - key: string, + key: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchResultOptions, @@ -3312,8 +3322,8 @@ export function createGeoSearch( /** @internal */ export function createGeoSearchStore( - destination: string, - source: string, + destination: GlideString, + source: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchStoreResultOptions, @@ -3328,8 +3338,8 @@ function convertGeoSearchOptionsToArgs( searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchCommonResultOptions, -): string[] { - let args: string[] = []; +): GlideString[] { + let args: GlideString[] = []; if ("position" in searchFrom) { args = args.concat( diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0ee31d892f..6f0be067d3 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3353,8 +3353,8 @@ export class BaseTransaction> { * `true` in the options, returns the number of elements updated in the sorted set. */ public geoadd( - key: string, - membersToGeospatialData: Map, + key: GlideString, + membersToGeospatialData: Map, options?: GeoAddOptions, ): T { return this.addAndReturn( @@ -3394,7 +3394,7 @@ export class BaseTransaction> { * - The coordinates as a two item `array` of floating point `number`s. */ public geosearch( - key: string, + key: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchResultOptions, @@ -3428,8 +3428,8 @@ export class BaseTransaction> { * Command Response - The number of elements in the resulting sorted set stored at `destination`. */ public geosearchstore( - destination: string, - source: string, + destination: GlideString, + source: GlideString, searchFrom: SearchOrigin, searchBy: GeoSearchShape, resultOptions?: GeoSearchStoreResultOptions, @@ -3458,7 +3458,7 @@ export class BaseTransaction> { * given members. The order of the returned positions matches the order of the input members. * If a member does not exist, its position will be `null`. */ - public geopos(key: string, members: string[]): T { + public geopos(key: GlideString, members: GlideString[]): T { return this.addAndReturn(createGeoPos(key, members)); } @@ -3561,9 +3561,9 @@ export class BaseTransaction> { * or if the key does not exist. */ public geodist( - key: string, - member1: string, - member2: string, + key: GlideString, + member1: GlideString, + member2: GlideString, geoUnit?: GeoUnit, ): T { return this.addAndReturn(createGeoDist(key, member1, member2, geoUnit)); @@ -3580,7 +3580,7 @@ export class BaseTransaction> { * Command Response - An array of `GeoHash` strings representing the positions of the specified members stored at `key`. * If a member does not exist in the sorted set, a `null` value is returned for that member. */ - public geohash(key: string, members: string[]): T { + public geohash(key: GlideString, members: GlideString[]): T { return this.addAndReturn(createGeoHash(key, members)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d2b63bb502..63bc68afeb 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -8961,6 +8961,111 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `geo commands binary %p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{geo-bin}-1-" + uuidv4(); + const key2 = "{geo-bin}-2-" + uuidv4(); + + const members = [ + "Catania", + Buffer.from("Palermo"), + "edge2", + "edge1", + ]; + const membersCoordinates: [number, number][] = [ + [15.087269, 37.502669], + [13.361389, 38.115556], + [17.24151, 38.788135], + [12.758489, 38.788135], + ]; + + const membersGeoData: GeospatialData[] = []; + + for (const [lon, lat] of membersCoordinates) { + membersGeoData.push({ longitude: lon, latitude: lat }); + } + + const membersToCoordinates = new Map(); + + for (let i = 0; i < members.length; i++) { + membersToCoordinates.set(members[i], membersGeoData[i]); + } + + // geoadd + expect( + await client.geoadd( + Buffer.from(key1), + membersToCoordinates, + ), + ).toBe(4); + // geopos + const geopos = await client.geopos(Buffer.from(key1), [ + "Palermo", + Buffer.from("Catania"), + "New York", + ]); + // inner array is possibly null, we need a null check or a cast + expect(geopos[0]?.[0]).toBeCloseTo(13.361389, 5); + expect(geopos[0]?.[1]).toBeCloseTo(38.115556, 5); + expect(geopos[1]?.[0]).toBeCloseTo(15.087269, 5); + expect(geopos[1]?.[1]).toBeCloseTo(37.502669, 5); + expect(geopos[2]).toBeNull(); + // geohash + const geohash = await client.geohash(Buffer.from(key1), [ + "Palermo", + Buffer.from("Catania"), + "New York", + ]); + expect(geohash).toEqual(["sqc8b49rny0", "sqdtr74hyu0", null]); + // geodist + expect( + await client.geodist( + Buffer.from(key1), + Buffer.from("Palermo"), + "Catania", + ), + ).toBeCloseTo(166274.1516, 5); + + // geosearch with binary decoder + let searchResult = await client.geosearch( + Buffer.from(key1), + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { decoder: Decoder.Bytes }, + ); + // using set to compare, because results are reordrered + expect(new Set(searchResult)).toEqual( + new Set(members.map((m) => Buffer.from(m))), + ); + // repeat geosearch with string decoder + searchResult = await client.geosearch( + Buffer.from(key1), + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ); + // using set to compare, because results are reordrered + expect(new Set(searchResult)).toEqual( + new Set(members.map((m) => m.toString())), + ); + // same with geosearchstore + expect( + await client.geosearchstore( + Buffer.from(key2), + Buffer.from(key1), + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `touch test_%p`, async (protocol) => { From 860a646d4dcc14331af4a1506efa8137710ecf33 Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Thu, 22 Aug 2024 14:05:53 -0700 Subject: [PATCH 210/236] Adding node lpos (#2185) Signed-off-by: Prateek Kumar --- node/src/BaseClient.ts | 6 +++--- node/src/Commands.ts | 6 +++--- node/src/Transaction.ts | 8 ++++++-- node/tests/SharedTests.ts | 7 +++++++ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index c9ff56dfb9..0cd58b152d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -5686,7 +5686,7 @@ export class BaseClient { * * @param key - The name of the list. * @param element - The value to search for within the list. - * @param options - The LPOS options. + * @param options - (Optional) The LPOS options - see {@link LPosOptions}. * @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. * @@ -5698,8 +5698,8 @@ export class BaseClient { * ``` */ public async lpos( - key: string, - element: string, + key: GlideString, + element: GlideString, options?: LPosOptions, ): Promise { return this.createWritePromise(createLPos(key, element, options)); diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b4a1b88fae..97f2a7e186 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3070,11 +3070,11 @@ export type LPosOptions = { * @internal */ export function createLPos( - key: string, - element: string, + key: GlideString, + element: GlideString, options?: LPosOptions, ): command_request.Command { - const args: string[] = [key, element]; + const args: GlideString[] = [key, element]; if (options) { if (options.rank !== undefined) { diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 6f0be067d3..f568ca7dd2 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3300,12 +3300,16 @@ export class BaseTransaction> { * * @param key - The name of the list. * @param element - The value to search for within the list. - * @param options - The LPOS options. + * @param options - (Optional) The LPOS options - see {@link LPosOptions}. * * 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. */ - public lpos(key: string, element: string, options?: LPosOptions): T { + public lpos( + key: GlideString, + element: GlideString, + options?: LPosOptions, + ): T { return this.addAndReturn(createLPos(key, element, options)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 63bc68afeb..e32eead7cb 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -7708,6 +7708,13 @@ export function runBaseTests(config: { // reverse traversal expect(await client.lpos(key, "b", { rank: -2 })).toEqual(2); + // reverse traversal with binary key and element. + expect( + await client.lpos(Buffer.from(key), Buffer.from("b"), { + rank: -2, + }), + ).toEqual(2); + // unlimited comparisons expect( await client.lpos(key, "a", { rank: 1, maxLength: 0 }), From 4c6ea2c92e56991f32b365e8ad35b8ec4f995066 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 22 Aug 2024 14:06:34 -0700 Subject: [PATCH 211/236] Node: Add binary variant to generic commands. (#2158) * Add binary variant to generic commands. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 114 ++++++++++-------- node/src/Commands.ts | 96 +++++++++------- node/src/GlideClient.ts | 45 +++++--- node/src/GlideClusterClient.ts | 47 +++++--- node/src/Transaction.ts | 160 +++++++++++++++----------- node/tests/GlideClient.test.ts | 47 ++++++-- node/tests/GlideClusterClient.test.ts | 62 ++++++++-- node/tests/SharedTests.ts | 108 ++++++++++++----- 9 files changed, 441 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4601ec6280..3bbfef3e09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to generic commands ([#2158](https://github.com/valkey-io/valkey-glide/pull/2158)) * Node: Added binary variant to geo commands ([#2149](https://github.com/valkey-io/valkey-glide/pull/2149)) * Node: Added binary variant to HYPERLOGLOG commands ([#2176](https://github.com/valkey-io/valkey-glide/pull/2176)) * Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129), [#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0cd58b152d..09976dd35b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1087,12 +1087,13 @@ export class BaseClient { return this.createWritePromise(createSet(key, value, options)); } - /** Removes the specified keys. A key is ignored if it does not exist. + /** + * Removes the specified keys. A key is ignored if it does not exist. * * @see {@link https://valkey.io/commands/del/|valkey.io} for details. * - * @param keys - the keys we wanted to remove. - * @returns the number of keys that were removed. + * @param keys - The keys we wanted to remove. + * @returns The number of keys that were removed. * * @example * ```typescript @@ -1109,7 +1110,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public async del(keys: string[]): Promise { + public async del(keys: GlideString[]): Promise { return this.createWritePromise(createDel(keys)); } @@ -2982,7 +2983,8 @@ export class BaseClient { return this.createWritePromise(createSRandMember(key, count)); } - /** Returns the number of keys in `keys` that exist in the database. + /** + * Returns the number of keys in `keys` that exist in the database. * * @see {@link https://valkey.io/commands/exists/|valkey.io} for details. * @@ -2997,13 +2999,14 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that all three keys exist in the database. * ``` */ - public async exists(keys: string[]): Promise { + public async exists(keys: GlideString[]): Promise { return this.createWritePromise(createExists(keys)); } - /** Removes the specified keys. A key is ignored if it does not exist. - * This command, similar to DEL, removes specified keys and ignores non-existent ones. - * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + /** + * Removes the specified keys. A key is ignored if it does not exist. + * This command, similar to {@link del}, removes specified keys and ignores non-existent ones. + * However, this command does not block the server, while {@link https://valkey.io/commands/del|`DEL`} does. * * @see {@link https://valkey.io/commands/unlink/|valkey.io} for details. * @@ -3017,11 +3020,12 @@ export class BaseClient { * console.log(result); // Output: 3 - Indicates that all three keys were unlinked from the database. * ``` */ - public async unlink(keys: string[]): Promise { + public async unlink(keys: GlideString[]): Promise { return this.createWritePromise(createUnlink(keys)); } - /** Sets a timeout on `key` in seconds. After the timeout has expired, the key will automatically be deleted. + /** + * Sets a timeout on `key` in seconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. @@ -3030,7 +3034,7 @@ export class BaseClient { * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * @returns `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, * or operation skipped due to the provided arguments. * @@ -3049,14 +3053,15 @@ export class BaseClient { * ``` */ public async expire( - key: string, + key: GlideString, seconds: number, option?: ExpireOptions, ): Promise { return this.createWritePromise(createExpire(key, seconds, option)); } - /** Sets a timeout on `key`. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of specifying the number of seconds. + /** + * Sets a timeout on `key`. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of specifying the number of seconds. * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. @@ -3065,7 +3070,7 @@ export class BaseClient { * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * @returns `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, * or operation skipped due to the provided arguments. * @@ -3077,7 +3082,7 @@ export class BaseClient { * ``` */ public async expireAt( - key: string, + key: GlideString, unixSeconds: number, option?: ExpireOptions, ): Promise { @@ -3110,11 +3115,12 @@ export class BaseClient { * console.log(result3); // Output: 123456 - the Unix timestamp (in seconds) when "myKey" will expire. * ``` */ - public async expiretime(key: string): Promise { + public async expiretime(key: GlideString): Promise { return this.createWritePromise(createExpireTime(key)); } - /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. + /** + * Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. @@ -3123,7 +3129,7 @@ export class BaseClient { * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * @returns `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, * or operation skipped due to the provided arguments. * @@ -3135,7 +3141,7 @@ export class BaseClient { * ``` */ public async pexpire( - key: string, + key: GlideString, milliseconds: number, option?: ExpireOptions, ): Promise { @@ -3144,7 +3150,8 @@ export class BaseClient { ); } - /** Sets a timeout on `key`. It takes an absolute Unix timestamp (milliseconds since January 1, 1970) instead of specifying the number of milliseconds. + /** + * Sets a timeout on `key`. It takes an absolute Unix timestamp (milliseconds since January 1, 1970) instead of specifying the number of milliseconds. * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. @@ -3153,7 +3160,7 @@ export class BaseClient { * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * @returns `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, * or operation skipped due to the provided arguments. * @@ -3165,7 +3172,7 @@ export class BaseClient { * ``` */ public async pexpireAt( - key: string, + key: GlideString, unixMilliseconds: number, option?: ExpireOptions, ): Promise { @@ -3197,11 +3204,12 @@ export class BaseClient { * console.log(result3); // Output: 123456789 - the Unix timestamp (in milliseconds) when "myKey" will expire. * ``` */ - public async pexpiretime(key: string): Promise { + public async pexpiretime(key: GlideString): Promise { return this.createWritePromise(createPExpireTime(key)); } - /** Returns the remaining time to live of `key` that has a timeout. + /** + * Returns the remaining time to live of `key` that has a timeout. * * @see {@link https://valkey.io/commands/ttl/|valkey.io} for details. * @@ -3229,7 +3237,7 @@ export class BaseClient { * console.log(result); // Output: -2 - Indicates that the key doesn't exist. * ``` */ - public async ttl(key: string): Promise { + public async ttl(key: GlideString): Promise { return this.createWritePromise(createTTL(key)); } @@ -4133,7 +4141,8 @@ export class BaseClient { return this.createWritePromise(createStrlen(key)); } - /** Returns the string representation of the type of the value stored at `key`. + /** + * Returns the string representation of the type of the value stored at `key`. * * @see {@link https://valkey.io/commands/type/|valkey.io} for more details. * @@ -4156,8 +4165,10 @@ export class BaseClient { * console.log(type); // Output: 'list' * ``` */ - public async type(key: string): Promise { - return this.createWritePromise(createType(key)); + public async type(key: GlideString): Promise { + return this.createWritePromise(createType(key), { + decoder: Decoder.String, + }); } /** Removes and returns the members with the lowest scores from the sorted set stored at `key`. @@ -4282,7 +4293,8 @@ export class BaseClient { return this.createWritePromise(createBZPopMax(keys, timeout)); } - /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. + /** + * Returns the remaining time to live of `key` that has a timeout, in milliseconds. * * @see {@link https://valkey.io/commands/pttl/|valkey.io} for more details. * @@ -4310,7 +4322,7 @@ export class BaseClient { * console.log(result); // Output: -1 - Indicates that the key "key" has no associated expire. * ``` */ - public async pttl(key: string): Promise { + public async pttl(key: GlideString): Promise { return this.createWritePromise(createPTTL(key)); } @@ -5346,7 +5358,8 @@ export class BaseClient { ); } - /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to + /** + * Removes the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). * * @see {@link https://valkey.io/commands/persist/|valkey.io} for more details. @@ -5361,7 +5374,7 @@ export class BaseClient { * console.log(result); // Output: true - Indicates that the timeout associated with the key "my_key" was successfully removed. * ``` */ - public async persist(key: string): Promise { + public async persist(key: GlideString): Promise { return this.createWritePromise(createPersist(key)); } @@ -5384,8 +5397,10 @@ export class BaseClient { * console.log(result); // Output: OK - Indicates successful renaming of the key "old_key" to "new_key". * ``` */ - public async rename(key: string, newKey: string): Promise<"OK"> { - return this.createWritePromise(createRename(key, newKey)); + public async rename(key: GlideString, newKey: GlideString): Promise<"OK"> { + return this.createWritePromise(createRename(key, newKey), { + decoder: Decoder.String, + }); } /** @@ -5407,7 +5422,10 @@ export class BaseClient { * console.log(result); // Output: true - Indicates successful renaming of the key "old_key" to "new_key". * ``` */ - public async renamenx(key: string, newKey: string): Promise { + public async renamenx( + key: GlideString, + newKey: GlideString, + ): Promise { return this.createWritePromise(createRenameNX(key, newKey)); } @@ -5550,37 +5568,43 @@ export class BaseClient { }); } - /** Returns the internal encoding for the Valkey object stored at `key`. + /** + * Returns the internal encoding for the Valkey object stored at `key`. * * @see {@link https://valkey.io/commands/object-encoding/|valkey.io} for more details. * * @param key - The `key` of the object to get the internal encoding of. * @returns - If `key` exists, returns the internal encoding of the object stored at `key` as a string. - * Otherwise, returns None. + * Otherwise, returns `null`. + * * @example * ```typescript * const result = await client.objectEncoding("my_hash"); * console.log(result); // Output: "listpack" * ``` */ - public async objectEncoding(key: string): Promise { - return this.createWritePromise(createObjectEncoding(key)); + public async objectEncoding(key: GlideString): Promise { + return this.createWritePromise(createObjectEncoding(key), { + decoder: Decoder.String, + }); } - /** Returns the logarithmic access frequency counter of a Valkey object stored at `key`. + /** + * Returns the logarithmic access frequency counter of a Valkey object stored at `key`. * * @see {@link https://valkey.io/commands/object-freq/|valkey.io} for more details. * * @param key - The `key` of the object to get the logarithmic access frequency counter of. * @returns - If `key` exists, returns the logarithmic access frequency counter of the object * stored at `key` as a `number`. Otherwise, returns `null`. + * * @example * ```typescript * const result = await client.objectFreq("my_hash"); * console.log(result); // Output: 2 - The logarithmic access frequency counter of "my_hash". * ``` */ - public async objectFreq(key: string): Promise { + public async objectFreq(key: GlideString): Promise { return this.createWritePromise(createObjectFreq(key)); } @@ -5598,7 +5622,7 @@ export class BaseClient { * console.log(result); // Output: 13 - "my_hash" was last accessed 13 seconds ago. * ``` */ - public async objectIdletime(key: string): Promise { + public async objectIdletime(key: GlideString): Promise { return this.createWritePromise(createObjectIdletime(key)); } @@ -5617,7 +5641,7 @@ export class BaseClient { * console.log(result); // Output: 2 - "my_hash" has a reference count of 2. * ``` */ - public async objectRefcount(key: string): Promise { + public async objectRefcount(key: GlideString): Promise { return this.createWritePromise(createObjectRefcount(key)); } @@ -6280,7 +6304,7 @@ export class BaseClient { * console.log(result); // Output: 2 - The last access time of 2 keys has been updated. * ``` */ - public async touch(keys: string[]): Promise { + public async touch(keys: GlideString[]): Promise { return this.createWritePromise(createTouch(keys)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 97f2a7e186..b8261c7496 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -10,7 +10,7 @@ import { BaseClient, Decoder } from "src/BaseClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { GlideClient } from "src/GlideClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { GlideClusterClient } from "src/GlideClusterClient"; +import { GlideClusterClient, Routes } from "src/GlideClusterClient"; import { GlideString } from "./BaseClient"; import { command_request } from "./ProtobufMessage"; @@ -89,7 +89,7 @@ function createCommand( return singleCommand; } -/** An extension to command option types. */ +/** An extension to command option types with {@link Decoder}. */ export type DecoderOption = { /** * {@link Decoder} type which defines how to handle the response. @@ -98,6 +98,15 @@ export type DecoderOption = { decoder?: Decoder; }; +/** An extension to command option types with {@link Routes}. */ +export type RouteOption = { + /** + * Specifies the routing configuration for the command. + * The client will route the command to the nodes defined by `route`. + */ + route?: Routes; +}; + /** * @internal */ @@ -294,7 +303,7 @@ export function createInfo(options?: InfoOptions[]): command_request.Command { /** * @internal */ -export function createDel(keys: string[]): command_request.Command { +export function createDel(keys: GlideString[]): command_request.Command { return createCommand(RequestType.Del, keys); } @@ -1239,14 +1248,14 @@ export function createHVals(key: GlideString): command_request.Command { /** * @internal */ -export function createExists(keys: string[]): command_request.Command { +export function createExists(keys: GlideString[]): command_request.Command { return createCommand(RequestType.Exists, keys); } /** * @internal */ -export function createUnlink(keys: string[]): command_request.Command { +export function createUnlink(keys: GlideString[]): command_request.Command { return createCommand(RequestType.Unlink, keys); } @@ -1275,11 +1284,11 @@ export enum ExpireOptions { * @internal */ export function createExpire( - key: string, + key: GlideString, seconds: number, option?: ExpireOptions, ): command_request.Command { - const args: string[] = + const args = option == undefined ? [key, seconds.toString()] : [key, seconds.toString(), option]; @@ -1290,11 +1299,11 @@ export function createExpire( * @internal */ export function createExpireAt( - key: string, + key: GlideString, unixSeconds: number, option?: ExpireOptions, ): command_request.Command { - const args: string[] = + const args = option == undefined ? [key, unixSeconds.toString()] : [key, unixSeconds.toString(), option]; @@ -1304,7 +1313,7 @@ export function createExpireAt( /** * @internal */ -export function createExpireTime(key: string): command_request.Command { +export function createExpireTime(key: GlideString): command_request.Command { return createCommand(RequestType.ExpireTime, [key]); } @@ -1312,11 +1321,11 @@ export function createExpireTime(key: string): command_request.Command { * @internal */ export function createPExpire( - key: string, + key: GlideString, milliseconds: number, option?: ExpireOptions, ): command_request.Command { - const args: string[] = + const args = option == undefined ? [key, milliseconds.toString()] : [key, milliseconds.toString(), option]; @@ -1327,11 +1336,11 @@ export function createPExpire( * @internal */ export function createPExpireAt( - key: string, + key: GlideString, unixMilliseconds: number, option?: ExpireOptions, ): command_request.Command { - const args: string[] = + const args = option == undefined ? [key, unixMilliseconds.toString()] : [key, unixMilliseconds.toString(), option]; @@ -1341,14 +1350,14 @@ export function createPExpireAt( /** * @internal */ -export function createPExpireTime(key: string): command_request.Command { +export function createPExpireTime(key: GlideString): command_request.Command { return createCommand(RequestType.PExpireTime, [key]); } /** * @internal */ -export function createTTL(key: string): command_request.Command { +export function createTTL(key: GlideString): command_request.Command { return createCommand(RequestType.TTL, [key]); } @@ -1852,7 +1861,7 @@ export function createZRangeStore( /** * @internal */ -export function createType(key: string): command_request.Command { +export function createType(key: GlideString): command_request.Command { return createCommand(RequestType.Type, [key]); } @@ -1931,7 +1940,7 @@ export function createEcho(message: string): command_request.Command { /** * @internal */ -export function createPTTL(key: string): command_request.Command { +export function createPTTL(key: GlideString): command_request.Command { return createCommand(RequestType.PTTL, [key]); } @@ -1979,7 +1988,7 @@ export function createZRemRangeByScore( } /** @internal */ -export function createPersist(key: string): command_request.Command { +export function createPersist(key: GlideString): command_request.Command { return createCommand(RequestType.Persist, [key]); } @@ -2815,8 +2824,8 @@ export function createXGroupDestroy( * @internal */ export function createRename( - key: string, - newKey: string, + key: GlideString, + newKey: GlideString, ): command_request.Command { return createCommand(RequestType.Rename, [key, newKey]); } @@ -2825,8 +2834,8 @@ export function createRename( * @internal */ export function createRenameNX( - key: string, - newKey: string, + key: GlideString, + newKey: GlideString, ): command_request.Command { return createCommand(RequestType.RenameNX, [key, newKey]); } @@ -2862,28 +2871,34 @@ export function createPfMerge( /** * @internal */ -export function createObjectEncoding(key: string): command_request.Command { +export function createObjectEncoding( + key: GlideString, +): command_request.Command { return createCommand(RequestType.ObjectEncoding, [key]); } /** * @internal */ -export function createObjectFreq(key: string): command_request.Command { +export function createObjectFreq(key: GlideString): command_request.Command { return createCommand(RequestType.ObjectFreq, [key]); } /** * @internal */ -export function createObjectIdletime(key: string): command_request.Command { +export function createObjectIdletime( + key: GlideString, +): command_request.Command { return createCommand(RequestType.ObjectIdleTime, [key]); } /** * @internal */ -export function createObjectRefcount(key: string): command_request.Command { +export function createObjectRefcount( + key: GlideString, +): command_request.Command { return createCommand(RequestType.ObjectRefCount, [key]); } @@ -2948,15 +2963,14 @@ export function createFlushDB(mode?: FlushMode): command_request.Command { } /** - * * @internal */ export function createCopy( - source: string, - destination: string, + source: GlideString, + destination: GlideString, options?: { destinationDB?: number; replace?: boolean }, ): command_request.Command { - let args: string[] = [source, destination]; + let args = [source, destination]; if (options) { if (options.destinationDB !== undefined) { @@ -2975,7 +2989,7 @@ export function createCopy( * @internal */ export function createMove( - key: string, + key: GlideString, dbIndex: number, ): command_request.Command { return createCommand(RequestType.Move, [key, dbIndex.toString()]); @@ -3502,7 +3516,7 @@ export type SortOptions = SortBaseOptions & { * contains IDs of objects, `byPattern` can be used to sort these IDs based on an * attribute of the objects, like their weights or timestamps. */ - byPattern?: string; + byPattern?: GlideString; /** * A pattern used to retrieve external keys' values, instead of the elements at `key`. @@ -3517,7 +3531,7 @@ export type SortOptions = SortBaseOptions & { * be used to include the actual element from `key` being sorted. If not provided, only * the sorted elements themselves are returned. */ - getPatterns?: string[]; + getPatterns?: GlideString[]; }; type SortBaseOptions = { @@ -3558,16 +3572,16 @@ export type Limit = { /** @internal */ export function createSort( - key: string, + key: GlideString, options?: SortOptions, - destination?: string, + destination?: GlideString, ): command_request.Command { return createSortImpl(RequestType.Sort, key, options, destination); } /** @internal */ export function createSortReadOnly( - key: string, + key: GlideString, options?: SortOptions, ): command_request.Command { return createSortImpl(RequestType.SortReadOnly, key, options); @@ -3576,11 +3590,11 @@ export function createSortReadOnly( /** @internal */ function createSortImpl( cmd: RequestType, - key: string, + key: GlideString, options?: SortOptions, - destination?: string, + destination?: GlideString, ): command_request.Command { - const args: string[] = [key]; + const args = [key]; if (options) { if (options.limit) { @@ -3705,7 +3719,7 @@ export function createLCS( /** * @internal */ -export function createTouch(keys: string[]): command_request.Command { +export function createTouch(keys: GlideString[]): command_request.Command { return createCommand(RequestType.Touch, keys); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 053e3ddb16..568889849b 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -13,6 +13,7 @@ import { ReturnType, } from "./BaseClient"; import { + DecoderOption, FlushMode, FunctionListOptions, FunctionListResponse, @@ -448,8 +449,8 @@ export class GlideClient extends BaseClient { * ``` */ public async copy( - source: string, - destination: string, + source: GlideString, + destination: GlideString, options?: { destinationDB?: number; replace?: boolean }, ): Promise { return this.createWritePromise( @@ -473,7 +474,7 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: true * ``` */ - public async move(key: string, dbIndex: number): Promise { + public async move(key: GlideString, dbIndex: number): Promise { return this.createWritePromise(createMove(key, dbIndex)); } @@ -789,7 +790,8 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/sort/|valkey.io} for more details. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - The {@link SortOptions}. + * @param options - (Optional) The {@link SortOptions} and {@link DecoderOption}. + * * @returns An `Array` of sorted elements. * * @example @@ -802,10 +804,12 @@ export class GlideClient extends BaseClient { * ``` */ public async sort( - key: string, - options?: SortOptions, - ): Promise<(string | null)[]> { - return this.createWritePromise(createSort(key, options)); + key: GlideString, + options?: SortOptions & DecoderOption, + ): Promise<(GlideString | null)[]> { + return this.createWritePromise(createSort(key, options), { + decoder: options?.decoder, + }); } /** @@ -820,7 +824,7 @@ export class GlideClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - The {@link SortOptions}. + * @param options - (Optional) The {@link SortOptions} and {@link DecoderOption}. * @returns An `Array` of sorted elements * * @example @@ -833,10 +837,12 @@ export class GlideClient extends BaseClient { * ``` */ public async sortReadOnly( - key: string, - options?: SortOptions, - ): Promise<(string | null)[]> { - return this.createWritePromise(createSortReadOnly(key, options)); + key: GlideString, + options?: SortOptions & DecoderOption, + ): Promise<(GlideString | null)[]> { + return this.createWritePromise(createSortReadOnly(key, options), { + decoder: options?.decoder, + }); } /** @@ -853,7 +859,7 @@ export class GlideClient extends BaseClient { * * @param key - The key of the list, set, or sorted set to be sorted. * @param destination - The key where the sorted result will be stored. - * @param options - The {@link SortOptions}. + * @param options - (Optional) The {@link SortOptions}. * @returns The number of elements in the sorted key stored at `destination`. * * @example @@ -867,8 +873,8 @@ export class GlideClient extends BaseClient { * ``` */ public async sortStore( - key: string, - destination: string, + key: GlideString, + destination: GlideString, options?: SortOptions, ): Promise { return this.createWritePromise(createSort(key, options, destination)); @@ -881,6 +887,7 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/lastsave/|valkey.io} for more details. * * @returns `UNIX TIME` of the last DB save executed with success. + * * @example * ```typescript * const timestamp = await client.lastsave(); @@ -896,6 +903,8 @@ export class GlideClient extends BaseClient { * * @see {@link https://valkey.io/commands/randomkey/|valkey.io} for more details. * + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A random existing key name from the currently selected database. * * @example @@ -904,8 +913,8 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: "key12" - "key12" is a random existing key name from the currently selected database. * ``` */ - public async randomKey(): Promise { - return this.createWritePromise(createRandomKey()); + public async randomKey(decoder?: Decoder): Promise { + return this.createWritePromise(createRandomKey(), { decoder: decoder }); } /** diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index da299ec5a4..1cb8ac42c0 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -13,6 +13,7 @@ import { ReturnType, } from "./BaseClient"; import { + DecoderOption, FlushMode, FunctionListOptions, FunctionListResponse, @@ -20,6 +21,7 @@ import { FunctionStatsSingleResponse, InfoOptions, LolwutOptions, + RouteOption, SortClusterOptions, createClientGetName, createClientId, @@ -698,8 +700,8 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async copy( - source: string, - destination: string, + source: GlideString, + destination: GlideString, replace?: boolean, ): Promise { return this.createWritePromise( @@ -1209,7 +1211,7 @@ export class GlideClusterClient extends BaseClient { * @see {@link https://valkey.io/commands/sort/|valkey.io} for details. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - (Optional) {@link SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions} and {@link DecoderOption}. * @returns An `Array` of sorted elements. * * @example @@ -1220,10 +1222,12 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async sort( - key: string, - options?: SortClusterOptions, - ): Promise { - return this.createWritePromise(createSort(key, options)); + key: GlideString, + options?: SortClusterOptions & DecoderOption, + ): Promise { + return this.createWritePromise(createSort(key, options), { + decoder: options?.decoder, + }); } /** @@ -1237,7 +1241,7 @@ export class GlideClusterClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - (Optional) {@link SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions} and {@link DecoderOption}. * @returns An `Array` of sorted elements * * @example @@ -1248,10 +1252,12 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async sortReadOnly( - key: string, - options?: SortClusterOptions, - ): Promise { - return this.createWritePromise(createSortReadOnly(key, options)); + key: GlideString, + options?: SortClusterOptions & DecoderOption, + ): Promise { + return this.createWritePromise(createSortReadOnly(key, options), { + decoder: options?.decoder, + }); } /** @@ -1280,8 +1286,8 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async sortStore( - key: string, - destination: string, + key: GlideString, + destination: GlideString, options?: SortClusterOptions, ): Promise { return this.createWritePromise(createSort(key, options, destination)); @@ -1296,6 +1302,7 @@ export class GlideClusterClient extends BaseClient { * @param route - (Optional) 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 `UNIX TIME` of the last DB save executed with success. + * * @example * ```typescript * const timestamp = await client.lastsave(); @@ -1311,10 +1318,11 @@ export class GlideClusterClient extends BaseClient { /** * Returns a random existing key name. * + * The command will be routed to all primary nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/randomkey/|valkey.io} for details. * - * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, - * in which case the client will route the command to the nodes defined by `route`. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * @returns A random existing key name. * * @example @@ -1323,9 +1331,12 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: "key12" - "key12" is a random existing key name. * ``` */ - public async randomKey(route?: Routes): Promise { + public async randomKey( + options?: DecoderOption & RouteOption, + ): Promise { return this.createWritePromise(createRandomKey(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index f568ca7dd2..3f31655a75 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -415,14 +415,16 @@ export class BaseTransaction> { return this.addAndReturn(createInfo(options)); } - /** Remove the specified keys. A key is ignored if it does not exist. + /** + * Removes the specified keys. A key is ignored if it does not exist. + * * @see {@link https://valkey.io/commands/del/|valkey.io} for details. * * @param keys - A list of keys to be deleted from the database. * - * Command Response - the number of keys that were removed. + * Command Response - The number of keys that were removed. */ - public del(keys: string[]): T { + public del(keys: GlideString[]): T { return this.addAndReturn(createDel(keys)); } @@ -1522,63 +1524,75 @@ export class BaseTransaction> { return this.addAndReturn(createSRandMember(key, count)); } - /** Returns the number of keys in `keys` that exist in the database. + /** + * Returns the number of keys in `keys` that exist in the database. + * * @see {@link https://valkey.io/commands/exists/|valkey.io} for details. * * @param keys - The keys list to check. * * Command Response - the number of keys that exist. If the same existing key is mentioned in `keys` multiple times, - * it will be counted multiple times. + * it will be counted multiple times. */ - public exists(keys: string[]): T { + public exists(keys: GlideString[]): T { return this.addAndReturn(createExists(keys)); } - /** Removes the specified keys. A key is ignored if it does not exist. - * This command, similar to DEL, removes specified keys and ignores non-existent ones. - * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + /** + * Removes the specified keys. A key is ignored if it does not exist. + * This command, similar to {@link del}, removes specified keys and ignores non-existent ones. + * However, this command does not block the server, while {@link https://valkey.io/commands/del|`DEL`} does. + * * @see {@link https://valkey.io/commands/unlink/|valkey.io} for details. * * @param keys - The keys we wanted to unlink. * - * Command Response - the number of keys that were unlinked. + * Command Response - The number of keys that were unlinked. */ - public unlink(keys: string[]): T { + public unlink(keys: GlideString[]): T { return this.addAndReturn(createUnlink(keys)); } - /** Sets a timeout on `key` in seconds. After the timeout has expired, the key will automatically be deleted. + /** + * Sets a timeout on `key` in seconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. + * * @see {@link https://valkey.io/commands/expire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * * Command Response - `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, - * or operation skipped due to the provided arguments. + * or operation skipped due to the provided arguments. */ - public expire(key: string, seconds: number, option?: ExpireOptions): T { + public expire( + key: GlideString, + seconds: number, + option?: ExpireOptions, + ): T { return this.addAndReturn(createExpire(key, seconds, option)); } - /** Sets a timeout on `key`. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of specifying the number of seconds. + /** + * Sets a timeout on `key`. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of specifying the number of seconds. * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. + * * @see {@link https://valkey.io/commands/expireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * * Command Response - `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, - * or operation skipped due to the provided arguments. + * or operation skipped due to the provided arguments. */ public expireAt( - key: string, + key: GlideString, unixSeconds: number, option?: ExpireOptions, ): T { @@ -1596,46 +1610,50 @@ export class BaseTransaction> { * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ - public expireTime(key: string): T { + public expireTime(key: GlideString): T { return this.addAndReturn(createExpireTime(key)); } - /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. + /** + * Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. + * * @see {@link https://valkey.io/commands/pexpire/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * * Command Response - `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, - * or operation skipped due to the provided arguments. + * or operation skipped due to the provided arguments. */ public pexpire( - key: string, + key: GlideString, milliseconds: number, option?: ExpireOptions, ): T { return this.addAndReturn(createPExpire(key, milliseconds, option)); } - /** Sets a timeout on `key`. It takes an absolute Unix timestamp (milliseconds since January 1, 1970) instead of specifying the number of milliseconds. + /** + * Sets a timeout on `key`. It takes an absolute Unix timestamp (milliseconds since January 1, 1970) instead of specifying the number of milliseconds. * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. + * * @see {@link https://valkey.io/commands/pexpireat/|valkey.io} for details. * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. - * @param option - The expire option. + * @param option - (Optional) The expire option - see {@link ExpireOptions}. * * Command Response - `true` if the timeout was set. `false` if the timeout was not set. e.g. key doesn't exist, - * or operation skipped due to the provided arguments. + * or operation skipped due to the provided arguments. */ public pexpireAt( - key: string, + key: GlideString, unixMilliseconds: number, option?: ExpireOptions, ): T { @@ -1654,18 +1672,20 @@ export class BaseTransaction> { * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ - public pexpireTime(key: string): T { + public pexpireTime(key: GlideString): T { return this.addAndReturn(createPExpireTime(key)); } - /** Returns the remaining time to live of `key` that has a timeout. + /** + * Returns the remaining time to live of `key` that has a timeout. + * * @see {@link https://valkey.io/commands/ttl/|valkey.io} for details. * * @param key - The key to return its timeout. * * Command Response - TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. */ - public ttl(key: string): T { + public ttl(key: GlideString): T { return this.addAndReturn(createTTL(key)); } @@ -2110,14 +2130,16 @@ export class BaseTransaction> { return this.addAndReturn(createZRandMember(key, count, true)); } - /** Returns the string representation of the type of the value stored at `key`. + /** + * Returns the string representation of the type of the value stored at `key`. + * * @see {@link https://valkey.io/commands/type/|valkey.io} for details. * * @param key - The key to check its data type. * * Command Response - If the key exists, the type of the stored value is returned. Otherwise, a "none" string is returned. */ - public type(key: string): T { + public type(key: GlideString): T { return this.addAndReturn(createType(key)); } @@ -2214,14 +2236,16 @@ export class BaseTransaction> { return this.addAndReturn(createEcho(message)); } - /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. + /** + * Returns the remaining time to live of `key` that has a timeout, in milliseconds. + * * @see {@link https://valkey.io/commands/pttl/|valkey.io} for more details. * * @param key - The key to return its timeout. * * Command Response - TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. */ - public pttl(key: string): T { + public pttl(key: GlideString): T { return this.addAndReturn(createPTTL(key)); } @@ -2370,15 +2394,17 @@ export class BaseTransaction> { return this.addAndReturn(createZRevRankWithScore(key, member)); } - /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to + /** + * Removes the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). + * * @see {@link https://valkey.io/commands/persist/|valkey.io} for details. * * @param key - The key to remove the existing timeout on. * * Command Response - `false` if `key` does not exist or does not have an associated timeout, `true` if the timeout has been removed. */ - public persist(key: string): T { + public persist(key: GlideString): T { return this.addAndReturn(createPersist(key)); } @@ -2940,8 +2966,6 @@ export class BaseTransaction> { /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. - * In Cluster mode, both `key` and `newkey` must be in the same hash slot, - * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. * * @see {@link https://valkey.io/commands/rename/|valkey.io} for details. * @@ -2950,23 +2974,22 @@ export class BaseTransaction> { * * Command Response - If the `key` was successfully renamed, return "OK". If `key` does not exist, an error is thrown. */ - public rename(key: string, newKey: string): T { + public rename(key: GlideString, newKey: GlideString): T { return this.addAndReturn(createRename(key, newKey)); } /** * Renames `key` to `newkey` if `newkey` does not yet exist. - * In Cluster mode, both `key` and `newkey` must be in the same hash slot, - * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. * * @see {@link https://valkey.io/commands/renamenx/|valkey.io} for details. * * @param key - The key to rename. * @param newKey - The new name of the key. + * * Command Response - If the `key` was successfully renamed, returns `true`. Otherwise, returns `false`. - * If `key` does not exist, an error is thrown. + * If `key` does not exist, an error is thrown. */ - public renamenx(key: string, newKey: string): T { + public renamenx(key: GlideString, newKey: GlideString): T { return this.addAndReturn(createRenameNX(key, newKey)); } @@ -3048,27 +3071,31 @@ export class BaseTransaction> { return this.addAndReturn(createPfMerge(destination, sourceKeys)); } - /** Returns the internal encoding for the Redis object stored at `key`. + /** + * Returns the internal encoding for the Redis object stored at `key`. * * @see {@link https://valkey.io/commands/object-encoding/|valkey.io} for more details. * * @param key - The `key` of the object to get the internal encoding of. + * * Command Response - If `key` exists, returns the internal encoding of the object stored at `key` as a string. * Otherwise, returns None. */ - public objectEncoding(key: string): T { + public objectEncoding(key: GlideString): T { return this.addAndReturn(createObjectEncoding(key)); } - /** Returns the logarithmic access frequency counter of a Redis object stored at `key`. + /** + * Returns the logarithmic access frequency counter of a Redis object stored at `key`. * * @see {@link https://valkey.io/commands/object-freq/|valkey.io} for more details. * * @param key - The `key` of the object to get the logarithmic access frequency counter of. + * * Command Response - If `key` exists, returns the logarithmic access frequency counter of * the object stored at `key` as a `number`. Otherwise, returns `null`. */ - public objectFreq(key: string): T { + public objectFreq(key: GlideString): T { return this.addAndReturn(createObjectFreq(key)); } @@ -3081,7 +3108,7 @@ export class BaseTransaction> { * * Command Response - If `key` exists, returns the idle time in seconds. Otherwise, returns `null`. */ - public objectIdletime(key: string): T { + public objectIdletime(key: GlideString): T { return this.addAndReturn(createObjectIdletime(key)); } @@ -3093,9 +3120,9 @@ export class BaseTransaction> { * @param key - The `key` of the object to get the reference count of. * * Command Response - If `key` exists, returns the reference count of the object stored at `key` as a `number`. - * Otherwise, returns `null`. + * Otherwise, returns `null`. */ - public objectRefcount(key: string): T { + public objectRefcount(key: GlideString): T { return this.addAndReturn(createObjectRefcount(key)); } @@ -3671,7 +3698,7 @@ export class BaseTransaction> { * * Command Response - The number of keys that were updated. A key is ignored if it doesn't exist. */ - public touch(keys: string[]): T { + public touch(keys: GlideString[]): T { return this.addAndReturn(createTouch(keys)); } @@ -3855,7 +3882,7 @@ export class Transaction extends BaseTransaction { * * Command Response - An `Array` of sorted elements. */ - public sort(key: string, options?: SortOptions): Transaction { + public sort(key: GlideString, options?: SortOptions): Transaction { return this.addAndReturn(createSort(key, options)); } @@ -3874,7 +3901,7 @@ export class Transaction extends BaseTransaction { * * Command Response - An `Array` of sorted elements */ - public sortReadOnly(key: string, options?: SortOptions): Transaction { + public sortReadOnly(key: GlideString, options?: SortOptions): Transaction { return this.addAndReturn(createSortReadOnly(key, options)); } @@ -3896,8 +3923,8 @@ export class Transaction extends BaseTransaction { * Command Response - The number of elements in the sorted key stored at `destination`. */ public sortStore( - key: string, - destination: string, + key: GlideString, + destination: GlideString, options?: SortOptions, ): Transaction { return this.addAndReturn(createSort(key, options, destination)); @@ -3922,8 +3949,8 @@ export class Transaction extends BaseTransaction { * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. */ public copy( - source: string, - destination: string, + source: GlideString, + destination: GlideString, options?: { destinationDB?: number; replace?: boolean }, ): Transaction { return this.addAndReturn(createCopy(source, destination, options)); @@ -3940,7 +3967,7 @@ export class Transaction extends BaseTransaction { * Command Response - `true` if `key` was moved, or `false` if the `key` already exists in the destination * database or does not exist in the source database. */ - public move(key: string, dbIndex: number): Transaction { + public move(key: GlideString, dbIndex: number): Transaction { return this.addAndReturn(createMove(key, dbIndex)); } @@ -3988,7 +4015,10 @@ export class ClusterTransaction extends BaseTransaction { * * Command Response - An `Array` of sorted elements. */ - public sort(key: string, options?: SortClusterOptions): ClusterTransaction { + public sort( + key: GlideString, + options?: SortClusterOptions, + ): ClusterTransaction { return this.addAndReturn(createSort(key, options)); } @@ -4009,7 +4039,7 @@ export class ClusterTransaction extends BaseTransaction { * Command Response - An `Array` of sorted elements */ public sortReadOnly( - key: string, + key: GlideString, options?: SortClusterOptions, ): ClusterTransaction { return this.addAndReturn(createSortReadOnly(key, options)); @@ -4033,8 +4063,8 @@ export class ClusterTransaction extends BaseTransaction { * Command Response - The number of elements in the sorted key stored at `destination`. */ public sortStore( - key: string, - destination: string, + key: GlideString, + destination: GlideString, options?: SortClusterOptions, ): ClusterTransaction { return this.addAndReturn(createSort(key, options, destination)); @@ -4055,8 +4085,8 @@ export class ClusterTransaction extends BaseTransaction { * Command Response - `true` if `source` was copied, `false` if the `source` was not copied. */ public copy( - source: string, - destination: string, + source: GlideString, + destination: GlideString, replace?: boolean, ): ClusterTransaction { return this.addAndReturn( diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 5167307d07..484eab350e 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -517,13 +517,13 @@ describe("GlideClient", () => { // no REPLACE, copying to existing key on DB 1, non-existing key on DB 2 expect( - await client.copy(source, destination, { + await client.copy(Buffer.from(source), destination, { destinationDB: index1, replace: false, }), ).toEqual(false); expect( - await client.copy(source, destination, { + await client.copy(source, Buffer.from(destination), { destinationDB: index2, replace: false, }), @@ -539,10 +539,14 @@ describe("GlideClient", () => { // destination expect(await client.select(index0)).toEqual("OK"); expect( - await client.copy(source, destination, { - destinationDB: index1, - replace: true, - }), + await client.copy( + Buffer.from(source), + Buffer.from(destination), + { + destinationDB: index1, + replace: true, + }, + ), ).toEqual(true); expect(await client.select(index1)).toEqual("OK"); expect(await client.get(destination)).toEqual(value2); @@ -581,7 +585,7 @@ describe("GlideClient", () => { expect(await client.set(key1, value)).toEqual("OK"); expect(await client.get(key1)).toEqual(value); - expect(await client.move(key1, 1)).toEqual(true); + expect(await client.move(Buffer.from(key1), 1)).toEqual(true); expect(await client.get(key1)).toEqual(null); expect(await client.select(1)).toEqual("OK"); expect(await client.get(key1)).toEqual(value); @@ -1129,7 +1133,7 @@ describe("GlideClient", () => { ).toEqual(["Alice", "Bob"]); expect( - await client.sort(list, { + await client.sort(Buffer.from(list), { limit: { offset: 0, count: 2 }, getPatterns: [setPrefix + "*->name"], orderBy: SortOrder.DESC, @@ -1145,6 +1149,22 @@ describe("GlideClient", () => { }), ).toEqual(["Eve", "40", "Charlie", "35"]); + // test binary decoder + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name", setPrefix + "*->age"], + orderBy: SortOrder.DESC, + decoder: Decoder.Bytes, + }), + ).toEqual([ + Buffer.from("Eve"), + Buffer.from("40"), + Buffer.from("Charlie"), + Buffer.from("35"), + ]); + // Non-existent key in the BY pattern will result in skipping the sorting operation expect(await client.sort(list, { byPattern: "noSort" })).toEqual([ "3", @@ -1186,11 +1206,12 @@ describe("GlideClient", () => { limit: { offset: 0, count: 2 }, getPatterns: [setPrefix + "*->name"], orderBy: SortOrder.DESC, + decoder: Decoder.Bytes, }), - ).toEqual(["Eve", "Dave"]); + ).toEqual([Buffer.from("Eve"), Buffer.from("Dave")]); expect( - await client.sortReadOnly(list, { + await client.sortReadOnly(Buffer.from(list), { limit: { offset: 0, count: 2 }, byPattern: setPrefix + "*->age", getPatterns: [ @@ -1242,7 +1263,7 @@ describe("GlideClient", () => { "Eve", ]); expect( - await client.sortStore(list, store, { + await client.sortStore(Buffer.from(list), store, { byPattern: setPrefix + "*->age", getPatterns: [setPrefix + "*->name"], }), @@ -1341,6 +1362,10 @@ describe("GlideClient", () => { expect(await client.set(key, "foo")).toEqual("OK"); // `key` should be the only key in the database expect(await client.randomKey()).toEqual(key); + // test binary decoder + expect(await client.randomKey(Decoder.Bytes)).toEqual( + Buffer.from(key), + ); // switch back to DB 0 expect(await client.select(0)).toEqual("OK"); diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 88c2af5076..538ce8c45a 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -637,25 +637,36 @@ describe("GlideClusterClient", () => { // neither key exists expect(await client.copy(source, destination, true)).toEqual(false); - expect(await client.copy(source, destination)).toEqual(false); + expect(await client.copy(Buffer.from(source), destination)).toEqual( + false, + ); // source exists, destination does not expect(await client.set(source, value1)).toEqual("OK"); - expect(await client.copy(source, destination, false)).toEqual(true); + expect( + await client.copy(source, Buffer.from(destination), false), + ).toEqual(true); expect(await client.get(destination)).toEqual(value1); // new value for source key expect(await client.set(source, value2)).toEqual("OK"); // both exists, no REPLACE - expect(await client.copy(source, destination)).toEqual(false); + expect( + await client.copy( + Buffer.from(source), + Buffer.from(destination), + ), + ).toEqual(false); expect(await client.copy(source, destination, false)).toEqual( false, ); expect(await client.get(destination)).toEqual(value1); // both exists, with REPLACE - expect(await client.copy(source, destination, true)).toEqual(true); + expect( + await client.copy(source, Buffer.from(destination), true), + ).toEqual(true); expect(await client.get(destination)).toEqual(value2); //transaction tests @@ -713,21 +724,44 @@ describe("GlideClusterClient", () => { expect(await client.sort(key3)).toEqual([]); expect(await client.lpush(key1, ["2", "1", "4", "3"])).toEqual(4); - expect(await client.sort(key1)).toEqual(["1", "2", "3", "4"]); + expect(await client.sort(Buffer.from(key1))).toEqual([ + "1", + "2", + "3", + "4", + ]); + // test binary decoder + expect(await client.sort(key1, { decoder: Decoder.Bytes })).toEqual( + [ + Buffer.from("1"), + Buffer.from("2"), + Buffer.from("3"), + Buffer.from("4"), + ], + ); // sort RO if (!cluster.checkIfServerVersionLessThan("7.0.0")) { expect(await client.sortReadOnly(key3)).toEqual([]); - expect(await client.sortReadOnly(key1)).toEqual([ - "1", - "2", - "3", - "4", + expect(await client.sortReadOnly(Buffer.from(key3))).toEqual( + [], + ); + // test binary decoder + expect( + await client.sortReadOnly(key1, { decoder: Decoder.Bytes }), + ).toEqual([ + Buffer.from("1"), + Buffer.from("2"), + Buffer.from("3"), + Buffer.from("4"), ]); } // sort with store expect(await client.sortStore(key1, key2)).toEqual(4); + expect( + await client.sortStore(Buffer.from(key1), Buffer.from(key2)), + ).toEqual(4); expect(await client.lrange(key2, 0, -1)).toEqual([ "1", "2", @@ -1598,8 +1632,12 @@ describe("GlideClusterClient", () => { expect(await client.set(key, "foo")).toEqual("OK"); // `key` should be the only existing key, so randomKey should return `key` - expect(await client.randomKey()).toEqual(key); - expect(await client.randomKey("allPrimaries")).toEqual(key); + expect(await client.randomKey({ decoder: Decoder.Bytes })).toEqual( + Buffer.from(key), + ); + expect(await client.randomKey({ route: "allPrimaries" })).toEqual( + key, + ); client.close(); }, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index e32eead7cb..9a7ec5fd7e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -239,7 +239,11 @@ export function runBaseTests(config: { expect(result).toEqual("OK"); result = await client.set(key3, value); expect(result).toEqual("OK"); - let deletedKeysNum = await client.del([key1, key2, key3]); + let deletedKeysNum = await client.del([ + key1, + Buffer.from(key2), + key3, + ]); expect(deletedKeysNum).toEqual(3); deletedKeysNum = await client.del([uuidv4()]); expect(deletedKeysNum).toEqual(0); @@ -3337,7 +3341,11 @@ export function runBaseTests(config: { expect(await client.exists([key1])).toEqual(1); expect(await client.set(key2, value)).toEqual("OK"); expect( - await client.exists([key1, "nonExistingKey", key2]), + await client.exists([ + key1, + "nonExistingKey", + Buffer.from(key2), + ]), ).toEqual(2); expect(await client.exists([key1, key1])).toEqual(2); }, protocol); @@ -3357,7 +3365,12 @@ export function runBaseTests(config: { expect(await client.set(key2, value)).toEqual("OK"); expect(await client.set(key3, value)).toEqual("OK"); expect( - await client.unlink([key1, key2, "nonExistingKey", key3]), + await client.unlink([ + key1, + key2, + "nonExistingKey", + Buffer.from(key3), + ]), ).toEqual(3); }, protocol); }, @@ -3378,26 +3391,32 @@ export function runBaseTests(config: { cluster.checkIfServerVersionLessThan("7.0.0"); if (versionLessThan) { - expect(await client.pexpire(key, 10000)).toEqual(true); + expect( + await client.pexpire(Buffer.from(key), 10000), + ).toEqual(true); } else { expect( await client.pexpire( - key, + Buffer.from(key), 10000, ExpireOptions.HasNoExpiry, ), ).toEqual(true); } - expect(await client.ttl(key)).toBeLessThanOrEqual(10); + expect(await client.ttl(Buffer.from(key))).toBeLessThanOrEqual( + 10, + ); /// TTL will be updated to the new value = 15 if (versionLessThan) { - expect(await client.expire(key, 15)).toEqual(true); + expect(await client.expire(Buffer.from(key), 15)).toEqual( + true, + ); } else { expect( await client.expire( - key, + Buffer.from(key), 15, ExpireOptions.HasExistingExpiry, ), @@ -3408,6 +3427,13 @@ export function runBaseTests(config: { expect(await client.pexpiretime(key)).toBeGreaterThan( Date.now(), ); + // test Buffer input argument + expect( + await client.expiretime(Buffer.from(key)), + ).toBeGreaterThan(Math.floor(Date.now() / 1000)); + expect( + await client.pexpiretime(Buffer.from(key)), + ).toBeGreaterThan(Date.now()); } expect(await client.ttl(key)).toBeLessThanOrEqual(15); @@ -3435,14 +3461,14 @@ export function runBaseTests(config: { if (versionLessThan) { expect( await client.expireAt( - key, + Buffer.from(key), Math.floor(Date.now() / 1000) + 50, ), ).toEqual(true); } else { expect( await client.expireAt( - key, + Buffer.from(key), Math.floor(Date.now() / 1000) + 50, ExpireOptions.NewExpiryGreaterThanCurrent, ), @@ -3462,6 +3488,14 @@ export function runBaseTests(config: { ExpireOptions.HasExistingExpiry, ), ).toEqual(false); + // test Buffer input argument + expect( + await client.pexpireAt( + Buffer.from(key), + Date.now() + 50000, + ExpireOptions.HasExistingExpiry, + ), + ).toEqual(false); } }, protocol); }, @@ -5293,7 +5327,7 @@ export function runBaseTests(config: { expect(await client.del([key])).toEqual(1); expect(await client.lpush(key, ["value"])).toEqual(1); - expect(await client.type(key)).toEqual("list"); + expect(await client.type(Buffer.from(key))).toEqual("list"); expect(await client.del([key])).toEqual(1); expect(await client.sadd(key, ["value"])).toEqual(1); @@ -5305,7 +5339,7 @@ export function runBaseTests(config: { expect(await client.del([key])).toEqual(1); expect(await client.hset(key, { field: "value" })).toEqual(1); - expect(await client.type(key)).toEqual("hash"); + expect(await client.type(Buffer.from(key))).toEqual("hash"); expect(await client.del([key])).toEqual(1); await client.xadd(key, [["field", "value"]]); @@ -5634,7 +5668,7 @@ export function runBaseTests(config: { expect(await client.pttl(key)).toEqual(-1); expect(await client.expire(key, 10)).toEqual(true); - let result = await client.pttl(key); + let result = await client.pttl(Buffer.from(key)); expect(result).toBeGreaterThan(0); expect(result).toBeLessThanOrEqual(10000); @@ -5847,7 +5881,7 @@ export function runBaseTests(config: { expect(await client.persist(key)).toEqual(false); expect(await client.expire(key, 10)).toEqual(true); - expect(await client.persist(key)).toEqual(true); + expect(await client.persist(Buffer.from(key))).toEqual(true); }, protocol); }, config.timeout, @@ -6766,11 +6800,15 @@ export function runBaseTests(config: { const key = uuidv4() + "{123}"; const newKey = uuidv4() + "{123}"; await client.set(key, "value"); - await client.rename(key, newKey); - const result = await client.get(newKey); - expect(result).toEqual("value"); + expect(await client.rename(key, newKey)).toEqual("OK"); + expect(await client.get(newKey)).toEqual("value"); // If key doesn't exist it should throw, it also test that key has successfully been renamed await expect(client.rename(key, newKey)).rejects.toThrow(); + // rename back + expect( + await client.rename(Buffer.from(newKey), Buffer.from(key)), + ).toEqual("OK"); + expect(await client.get(key)).toEqual("value"); }, protocol); }, config.timeout, @@ -6795,11 +6833,15 @@ export function runBaseTests(config: { await client.set(key1, "key1"); await client.set(key3, "key3"); // Test that renamenx can rename key1 to key2 (non-existing value) - expect(await client.renamenx(key1, key2)).toEqual(true); + expect(await client.renamenx(Buffer.from(key1), key2)).toEqual( + true, + ); // sanity check expect(await client.get(key2)).toEqual("key1"); // Test that renamenx doesn't rename key2 to key3 (with an existing value) - expect(await client.renamenx(key2, key3)).toEqual(false); + expect(await client.renamenx(key2, Buffer.from(key3))).toEqual( + false, + ); // sanity check expect(await client.get(key3)).toEqual("key3"); }, protocol); @@ -7439,7 +7481,9 @@ export function runBaseTests(config: { expect(await client.objectEncoding(string_key)).toEqual("raw"); expect(await client.set(string_key, "2")).toEqual("OK"); - expect(await client.objectEncoding(string_key)).toEqual("int"); + expect( + await client.objectEncoding(Buffer.from(string_key)), + ).toEqual("int"); expect(await client.set(string_key, "value")).toEqual("OK"); expect(await client.objectEncoding(string_key)).toEqual( @@ -7530,7 +7574,9 @@ export function runBaseTests(config: { if (versionLessThan7) { expect( - await client.objectEncoding(zset_listpack_key), + await client.objectEncoding( + Buffer.from(zset_listpack_key), + ), ).toEqual("ziplist"); } else { expect( @@ -7569,9 +7615,9 @@ export function runBaseTests(config: { null, ); expect(await client.set(key, "foobar")).toEqual("OK"); - expect(await client.objectFreq(key)).toBeGreaterThanOrEqual( - 0, - ); + expect( + await client.objectFreq(Buffer.from(key)), + ).toBeGreaterThanOrEqual(0); } finally { expect( await client.configSet({ @@ -7608,7 +7654,9 @@ export function runBaseTests(config: { await wait(2000); - expect(await client.objectIdletime(key)).toBeGreaterThan(0); + expect( + await client.objectIdletime(Buffer.from(key)), + ).toBeGreaterThan(0); } finally { expect( await client.configSet({ @@ -7636,9 +7684,9 @@ export function runBaseTests(config: { expect(await client.objectRefcount(nonExistingKey)).toBeNull(); expect(await client.set(key, "foo")).toEqual("OK"); - expect(await client.objectRefcount(key)).toBeGreaterThanOrEqual( - 1, - ); + expect( + await client.objectRefcount(Buffer.from(key)), + ).toBeGreaterThanOrEqual(1); }, protocol); }, config.timeout, @@ -9084,7 +9132,9 @@ export function runBaseTests(config: { expect( await client.mset({ [key1]: "value1", [key2]: "value2" }), ).toEqual("OK"); - expect(await client.touch([key1, key2])).toEqual(2); + expect(await client.touch([key1, Buffer.from(key2)])).toEqual( + 2, + ); expect( await client.touch([key2, nonExistingKey, key1]), ).toEqual(2); From 1bb9b137b89a952071eb841db52e4bb1fe5e316c Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Fri, 23 Aug 2024 00:37:42 +0300 Subject: [PATCH 212/236] Node - Add binary support to transaction commands (#2187) add binary to transaction commands Signed-off-by: lior sventitzky --- node/src/Transaction.ts | 46 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3f31655a75..7a6db5341f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -371,7 +371,7 @@ export class BaseTransaction> { * * Command Response - substring extracted from the value stored at `key`. */ - public getrange(key: string, start: number, end: number): T { + public getrange(key: GlideString, start: number, end: number): T { return this.addAndReturn(createGetRange(key, start, end)); } @@ -500,7 +500,7 @@ export class BaseTransaction> { * Command Response - A list of values corresponding to the provided keys. If a key is not found, * its corresponding value in the list will be null. */ - public mget(keys: string[]): T { + public mget(keys: GlideString[]): T { return this.addAndReturn(createMGet(keys)); } @@ -780,7 +780,7 @@ export class BaseTransaction> { * * Command Response - the value associated with `field`, or null when `field` is not present in the hash or `key` does not exist. */ - public hget(key: string, field: string): T { + public hget(key: GlideString, field: GlideString): T { return this.addAndReturn(createHGet(key, field)); } @@ -926,7 +926,7 @@ export class BaseTransaction> { * * Command Response - a list of values in the hash, or an empty list when the key does not exist. */ - public hvals(key: string): T { + public hvals(key: GlideString): T { return this.addAndReturn(createHVals(key)); } @@ -1026,7 +1026,7 @@ export class BaseTransaction> { * * Command Response - the length of the list after the push operations. */ - public lpush(key: string, elements: string[]): T { + public lpush(key: GlideString, elements: GlideString[]): T { return this.addAndReturn(createLPush(key, elements)); } @@ -1054,7 +1054,7 @@ export class BaseTransaction> { * Command Response - The value of the first element. * If `key` does not exist null will be returned. */ - public lpop(key: string): T { + public lpop(key: GlideString): T { return this.addAndReturn(createLPop(key)); } @@ -1067,7 +1067,7 @@ export class BaseTransaction> { * Command Response - A list of the popped elements will be returned depending on the list's length. * If `key` does not exist null will be returned. */ - public lpopCount(key: string, count: number): T { + public lpopCount(key: GlideString, count: number): T { return this.addAndReturn(createLPop(key, count)); } @@ -1086,7 +1086,7 @@ export class BaseTransaction> { * If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. * If `key` does not exist an empty list will be returned. */ - public lrange(key: string, start: number, end: number): T { + public lrange(key: GlideString, start: number, end: number): T { return this.addAndReturn(createLRange(key, start, end)); } @@ -1098,7 +1098,7 @@ export class BaseTransaction> { * Command Response - the length of the list at `key`. * If `key` does not exist, it is interpreted as an empty list and 0 is returned. */ - public llen(key: string): T { + public llen(key: GlideString): T { return this.addAndReturn(createLLen(key)); } @@ -1118,8 +1118,8 @@ export class BaseTransaction> { * Command Response - The popped element, or `null` if `source` does not exist. */ public lmove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, ): T { @@ -1149,8 +1149,8 @@ export class BaseTransaction> { * Command Response - The popped element, or `null` if `source` does not exist or if the operation timed-out. */ public blmove( - source: string, - destination: string, + source: GlideString, + destination: GlideString, whereFrom: ListDirection, whereTo: ListDirection, timeout: number, @@ -1223,7 +1223,7 @@ export class BaseTransaction> { * * Command Response - the length of the list after the push operations. */ - public rpush(key: string, elements: string[]): T { + public rpush(key: GlideString, elements: GlideString[]): T { return this.addAndReturn(createRPush(key, elements)); } @@ -1251,7 +1251,7 @@ export class BaseTransaction> { * Command Response - The value of the last element. * If `key` does not exist null will be returned. */ - public rpop(key: string): T { + public rpop(key: GlideString): T { return this.addAndReturn(createRPop(key)); } @@ -1264,7 +1264,7 @@ export class BaseTransaction> { * Command Response - A list of popped elements will be returned depending on the list's length. * If `key` does not exist null will be returned. */ - public rpopCount(key: string, count: number): T { + public rpopCount(key: GlideString, count: number): T { return this.addAndReturn(createRPop(key, count)); } @@ -2430,7 +2430,7 @@ export class BaseTransaction> { * Command Response - The element at index in the list stored at `key`. * If `index` is out of range or if `key` does not exist, null is returned. */ - public lindex(key: string, index: number): T { + public lindex(key: GlideString, index: number): T { return this.addAndReturn(createLIndex(key, index)); } @@ -2450,10 +2450,10 @@ export class BaseTransaction> { * If the `pivot` wasn't found, returns `0`. */ public linsert( - key: string, + key: GlideString, position: InsertPosition, - pivot: string, - element: string, + pivot: GlideString, + element: GlideString, ): T { return this.addAndReturn(createLInsert(key, position, pivot, element)); } @@ -3007,7 +3007,7 @@ export class BaseTransaction> { * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. */ - public brpop(keys: string[], timeout: number): T { + public brpop(keys: GlideString[], timeout: number): T { return this.addAndReturn(createBRPop(keys, timeout)); } @@ -3025,7 +3025,7 @@ export class BaseTransaction> { * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. */ - public blpop(keys: string[], timeout: number): T { + public blpop(keys: GlideString[], timeout: number): T { return this.addAndReturn(createBLPop(keys, timeout)); } @@ -3741,7 +3741,7 @@ export class BaseTransaction> { * * Command Response - The length of the string after appending the value. */ - public append(key: string, value: string): T { + public append(key: GlideString, value: GlideString): T { return this.addAndReturn(createAppend(key, value)); } From 3131719b4525b43fbbdb672fdcf1a6815ce48ae2 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 22 Aug 2024 17:46:21 -0700 Subject: [PATCH 213/236] Node: Add binary api for bitmap commands (#2178) * Node: Add binary api for bitmap commands Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 18 +++++++++--------- node/src/Commands.ts | 18 +++++++++--------- node/src/Transaction.ts | 18 +++++++++--------- node/tests/SharedTests.ts | 25 ++++++++++++++++++------- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbfef3e09..ba0dca22b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to bitmap commands ([#2178](https://github.com/valkey-io/valkey-glide/pull/2178)) * Node: Added binary variant to generic commands ([#2158](https://github.com/valkey-io/valkey-glide/pull/2158)) * Node: Added binary variant to geo commands ([#2149](https://github.com/valkey-io/valkey-glide/pull/2149)) * Node: Added binary variant to HYPERLOGLOG commands ([#2176](https://github.com/valkey-io/valkey-glide/pull/2176)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 09976dd35b..117f5ccaa7 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1383,8 +1383,8 @@ export class BaseClient { */ public async bitop( operation: BitwiseOperation, - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): Promise { return this.createWritePromise( createBitOp(operation, destination, keys), @@ -1408,7 +1408,7 @@ export class BaseClient { * console.log(result); // Output: 1 - The second bit of the string stored at "key" is set to 1. * ``` */ - public async getbit(key: string, offset: number): Promise { + public async getbit(key: GlideString, offset: number): Promise { return this.createWritePromise(createGetBit(key, offset)); } @@ -1432,7 +1432,7 @@ export class BaseClient { * ``` */ public async setbit( - key: string, + key: GlideString, offset: number, value: number, ): Promise { @@ -1464,7 +1464,7 @@ export class BaseClient { * ``` */ public async bitpos( - key: string, + key: GlideString, bit: number, start?: number, ): Promise { @@ -1505,7 +1505,7 @@ export class BaseClient { * ``` */ public async bitposInterval( - key: string, + key: GlideString, bit: number, start: number, end: number, @@ -1548,7 +1548,7 @@ export class BaseClient { * ``` */ public async bitfield( - key: string, + key: GlideString, subcommands: BitFieldSubCommands[], ): Promise<(number | null)[]> { return this.createWritePromise(createBitField(key, subcommands)); @@ -1572,7 +1572,7 @@ export class BaseClient { * ``` */ public async bitfieldReadOnly( - key: string, + key: GlideString, subcommands: BitFieldGet[], ): Promise { return this.createWritePromise(createBitField(key, subcommands, true)); @@ -5750,7 +5750,7 @@ export class BaseClient { * ``` */ public async bitcount( - key: string, + key: GlideString, options?: BitOffsetOptions, ): Promise { return this.createWritePromise(createBitCount(key, options)); diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b8261c7496..d99174cf70 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -490,8 +490,8 @@ export enum BitwiseOperation { */ export function createBitOp( operation: BitwiseOperation, - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): command_request.Command { return createCommand(RequestType.BitOp, [operation, destination, ...keys]); } @@ -500,7 +500,7 @@ export function createBitOp( * @internal */ export function createGetBit( - key: string, + key: GlideString, offset: number, ): command_request.Command { return createCommand(RequestType.GetBit, [key, offset.toString()]); @@ -510,7 +510,7 @@ export function createGetBit( * @internal */ export function createSetBit( - key: string, + key: GlideString, offset: number, value: number, ): command_request.Command { @@ -799,14 +799,14 @@ export class BitFieldOverflow implements BitFieldSubCommands { * @internal */ export function createBitField( - key: string, + key: GlideString, subcommands: BitFieldSubCommands[], readOnly: boolean = false, ): command_request.Command { const requestType = readOnly ? RequestType.BitFieldReadOnly : RequestType.BitField; - let args: string[] = [key]; + let args: GlideString[] = [key]; for (const subcommand of subcommands) { args = args.concat(subcommand.toArgs()); @@ -2438,7 +2438,7 @@ export type BitOffsetOptions = { * @internal */ export function createBitCount( - key: string, + key: GlideString, options?: BitOffsetOptions, ): command_request.Command { const args = [key]; @@ -2470,13 +2470,13 @@ export enum BitmapIndexType { * @internal */ export function createBitPos( - key: string, + key: GlideString, bit: number, start?: number, end?: number, indexType?: BitmapIndexType, ): command_request.Command { - const args = [key, bit.toString()]; + const args: GlideString[] = [key, bit.toString()]; if (start !== undefined) { args.push(start.toString()); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7a6db5341f..b2b53bf589 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -612,8 +612,8 @@ export class BaseTransaction> { */ public bitop( operation: BitwiseOperation, - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): T { return this.addAndReturn(createBitOp(operation, destination, keys)); } @@ -630,7 +630,7 @@ export class BaseTransaction> { * Command Response - The bit at the given `offset` of the string. Returns `0` if the key is empty or if the * `offset` exceeds the length of the string. */ - public getbit(key: string, offset: number): T { + public getbit(key: GlideString, offset: number): T { return this.addAndReturn(createGetBit(key, offset)); } @@ -648,7 +648,7 @@ export class BaseTransaction> { * * Command Response - The bit value that was previously stored at `offset`. */ - public setbit(key: string, offset: number, value: number): T { + public setbit(key: GlideString, offset: number, value: number): T { return this.addAndReturn(createSetBit(key, offset, value)); } @@ -667,7 +667,7 @@ export class BaseTransaction> { * Command Response - The position of the first occurrence of `bit` in the binary value of the string held at `key`. * If `start` was provided, the search begins at the offset indicated by `start`. */ - public bitpos(key: string, bit: number, start?: number): T { + public bitpos(key: GlideString, bit: number, start?: number): T { return this.addAndReturn(createBitPos(key, bit, start)); } @@ -696,7 +696,7 @@ export class BaseTransaction> { * binary value of the string held at `key`. */ public bitposInterval( - key: string, + key: GlideString, bit: number, start: number, end: number, @@ -729,7 +729,7 @@ export class BaseTransaction> { * subcommands when an overflow or underflow occurs. {@link BitFieldOverflow} does not return a value and * does not contribute a value to the array response. */ - public bitfield(key: string, subcommands: BitFieldSubCommands[]): T { + public bitfield(key: GlideString, subcommands: BitFieldSubCommands[]): T { return this.addAndReturn(createBitField(key, subcommands)); } @@ -745,7 +745,7 @@ export class BaseTransaction> { * Command Response - An array of results from the {@link BitFieldGet} subcommands. * */ - public bitfieldReadOnly(key: string, subcommands: BitFieldGet[]): T { + public bitfieldReadOnly(key: GlideString, subcommands: BitFieldGet[]): T { return this.addAndReturn(createBitField(key, subcommands, true)); } @@ -3364,7 +3364,7 @@ export class BaseTransaction> { * If `options` is not provided, returns the number of set bits in the string stored at `key`. * Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. */ - public bitcount(key: string, options?: BitOffsetOptions): T { + public bitcount(key: GlideString, options?: BitOffsetOptions): T { return this.addAndReturn(createBitCount(key, options)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 9a7ec5fd7e..e3073d33ee 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -591,6 +591,7 @@ export function runBaseTests(config: { const key2 = `{key}-${uuidv4()}`; const key3 = `{key}-${uuidv4()}`; const keys = [key1, key2]; + const keysEncoded = [Buffer.from(key1), Buffer.from(key2)]; const destination = `{key}-${uuidv4()}`; const nonExistingKey1 = `{key}-${uuidv4()}`; const nonExistingKey2 = `{key}-${uuidv4()}`; @@ -611,7 +612,11 @@ export function runBaseTests(config: { ).toEqual(6); expect(await client.get(destination)).toEqual("`bc`ab"); expect( - await client.bitop(BitwiseOperation.OR, destination, keys), + await client.bitop( + BitwiseOperation.OR, + destination, + keysEncoded, + ), ).toEqual(6); expect(await client.get(destination)).toEqual("goofev"); @@ -725,7 +730,7 @@ export function runBaseTests(config: { expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.getbit(key, 1)).toEqual(1); // When offset is beyond the string length, the string is assumed to be a contiguous space with 0 bits. - expect(await client.getbit(key, 1000)).toEqual(0); + expect(await client.getbit(Buffer.from(key), 1000)).toEqual(0); // When key does not exist it is assumed to be an empty string, so offset is always out of range and the // value is also assumed to be a contiguous space with 0 bits. expect(await client.getbit(nonExistingKey, 1)).toEqual(0); @@ -753,7 +758,7 @@ export function runBaseTests(config: { const setKey = `{key}-${uuidv4()}`; expect(await client.setbit(key, 1, 1)).toEqual(0); - expect(await client.setbit(key, 1, 0)).toEqual(1); + expect(await client.setbit(Buffer.from(key), 1, 0)).toEqual(1); // invalid argument - offset can't be negative await expect(client.setbit(key, -1, 1)).rejects.toThrow( @@ -786,9 +791,12 @@ export function runBaseTests(config: { expect(await client.set(key, value)).toEqual("OK"); expect(await client.bitpos(key, 0)).toEqual(0); - expect(await client.bitpos(key, 1)).toEqual(2); + expect(await client.bitpos(Buffer.from(key), 1)).toEqual(2); expect(await client.bitpos(key, 1, 1)).toEqual(9); expect(await client.bitposInterval(key, 0, 3, 5)).toEqual(24); + expect( + await client.bitposInterval(Buffer.from(key), 0, 3, 5), + ).toEqual(24); // -1 is returned if start > end expect(await client.bitposInterval(key, 0, 1, 0)).toEqual(-1); @@ -947,7 +955,7 @@ export function runBaseTests(config: { // INCRBY tests expect( - await client.bitfield(key1, [ + await client.bitfield(Buffer.from(key1), [ // binary value becomes: // 0(11)00011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 new BitFieldIncrBy(u2, offset1, 1), @@ -1104,7 +1112,7 @@ export function runBaseTests(config: { // offset is greater than current length of string: the operation is performed like the missing part all // consists of bits set to 0. expect( - await client.bitfieldReadOnly(key, [ + await client.bitfieldReadOnly(Buffer.from(key), [ new BitFieldGet( new UnsignedEncoding(3), new BitOffset(100), @@ -7884,7 +7892,10 @@ export function runBaseTests(config: { expect(await client.set(key1, value)).toEqual("OK"); expect(await client.bitcount(key1)).toEqual(26); expect( - await client.bitcount(key1, { start: 1, end: 1 }), + await client.bitcount(Buffer.from(key1), { + start: 1, + end: 1, + }), ).toEqual(6); expect( await client.bitcount(key1, { start: 0, end: -5 }), From a3cc46760105cb14799939317ba7eca09f28b590 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 23 Aug 2024 10:43:52 -0700 Subject: [PATCH 214/236] Java: Fix docs for stream commands. (#2086) * Fix docs. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + .../api/commands/StreamBaseCommands.java | 518 ++++++++++-------- .../glide/api/models/BaseTransaction.java | 107 ++-- 3 files changed, 352 insertions(+), 274 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0dca22b7..5bbe8db8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Java: Fix docs for stream commands ([#2086](https://github.com/valkey-io/valkey-glide/pull/2086)) * Node: Added binary variant to bitmap commands ([#2178](https://github.com/valkey-io/valkey-glide/pull/2178)) * Node: Added binary variant to generic commands ([#2158](https://github.com/valkey-io/valkey-glide/pull/2158)) * Node: Added binary variant to geo commands ([#2149](https://github.com/valkey-io/valkey-glide/pull/2149)) diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index 57bc86de78..b2b663aaa3 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -200,8 +200,9 @@ CompletableFuture xadd( * slot. * @see valkey.io for details. * @param keysAndIds A Map of keys and entry IDs to read from. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A {@literal Map>} with stream keys, to + * Map of stream entry IDs, to an array of pairings with format + * [[field, entry], [field, entry], ...]. * @example *
      {@code
            * Map xreadKeys = Map.of("streamKey", "0-0");
      @@ -210,9 +211,10 @@ CompletableFuture xadd(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
      +     * }
            * }
      */ CompletableFuture>> xread(Map keysAndIds); @@ -224,8 +226,9 @@ CompletableFuture xadd( * slot. * @see valkey.io for details. * @param keysAndIds A Map of keys and entry IDs to read from. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A {@literal Map>} with stream keys, to + * Map of stream entry IDs, to an array of pairings with format + * [[field, entry], [field, entry], ...]. * @example *
      {@code
            * Map xreadKeys = Map.of(gs("streamKey"), gs("0-0"));
      @@ -234,9 +237,10 @@ CompletableFuture xadd(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
      +     * }
            * }
      */ CompletableFuture>> xreadBinary( @@ -250,8 +254,9 @@ CompletableFuture>> xreadBina * @see valkey.io for details. * @param keysAndIds A Map of keys and entry IDs to read from. * @param options Options detailing how to read the stream {@link StreamReadOptions}. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A {@literal Map>} with stream keys, to + * Map of stream entry IDs, to an array of pairings with format + * [[field, entry], [field, entry], ...]. * @example *
      {@code
            * // retrieve streamKey entries and block for 1 second if is no stream data
      @@ -262,9 +267,10 @@ CompletableFuture>> xreadBina
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
      +     * }
            * }
      */ CompletableFuture>> xread( @@ -278,8 +284,10 @@ CompletableFuture>> xread( * @see valkey.io for details. * @param keysAndIds A Map of keys and entry IDs to read from. * @param options Options detailing how to read the stream {@link StreamReadOptions}. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A {@literal Map>} with + * stream keys, to Map of stream entry IDs, to an array of pairings with format + * + * [[field, entry], [field, entry], ...]. * @example *
      {@code
            * // retrieve streamKey entries and block for 1 second if is no stream data
      @@ -290,9 +298,10 @@ CompletableFuture>> xread(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
      +     * }
            * }
      */ CompletableFuture>> xreadBinary( @@ -407,34 +416,36 @@ CompletableFuture>> xreadBina * * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve all stream entries
            * Map result = client.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
            * });
            * // Retrieve exactly one stream entry by id
            * Map result = client.xrange("key", IdBound.of(streamId), IdBound.of(streamId)).get();
      -     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
      +     * System.out.println("stream entry ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
            * }
      */ CompletableFuture> xrange(String key, StreamRange start, StreamRange end); @@ -444,34 +455,36 @@ CompletableFuture>> xreadBina * * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve all stream entries
            * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
            * });
            * // Retrieve exactly one stream entry by id
            * Map result = client.xrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
      -     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
      +     * System.out.println("stream entry ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
            * }
      */ CompletableFuture> xrange( @@ -482,28 +495,30 @@ CompletableFuture> xrange( * * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * * @param count Maximum count of stream entries to return. - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve the first 2 stream entries
            * Map result = client.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX, 2).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
      @@ -518,28 +533,30 @@ CompletableFuture> xrange(
            *
            * @see valkey.io for details.
            * @param key The key of the stream.
      -     * @param start Starting stream ID bound for range.
      +     * @param start Starting stream entry ID bound for range.
            *     
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * * @param count Maximum count of stream entries to return. - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve the first 2 stream entries
            * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX, 2).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
      @@ -556,34 +573,36 @@ CompletableFuture> xrange(
            *
            * @see valkey.io for details.
            * @param key The key of the stream.
      -     * @param end Ending stream ID bound for range.
      +     * @param end Ending stream entry ID bound for range.
            *     
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve all stream entries
            * Map result = client.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
            * });
            * // Retrieve exactly one stream entry by id
            * Map result = client.xrevrange("key", IdBound.of(streamId), IdBound.of(streamId)).get();
      -     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
      +     * System.out.println("stream entry ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
            * }
      */ CompletableFuture> xrevrange( @@ -596,34 +615,35 @@ CompletableFuture> xrevrange( * * @see valkey.io for details. * @param key The key of the stream. - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. * @example *
      {@code
            * // Retrieve all stream entries
            * Map result = client.xrevrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
            * });
            * // Retrieve exactly one stream entry by id
            * Map result = client.xrevrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
      -     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
      +     * System.out.println("stream entry ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
            * }
      */ CompletableFuture> xrevrange( @@ -636,28 +656,30 @@ CompletableFuture> xrevrange( * * @see valkey.io for details. * @param key The key of the stream. - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * * @param count Maximum count of stream entries to return. - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve the first 2 stream entries
            * Map result = client.xrange("key", InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
      @@ -669,33 +691,35 @@ CompletableFuture> xrevrange(
       
           /**
            * Returns stream entries matching a given range of IDs in reverse order.
      - * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange, long)} but returns the entries - * in reverse order. + * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange, long)} but returns the + * entries in reverse order. * * @see valkey.io for details. * @param key The key of the stream. - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
      * - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
        - *
      • Use {@link IdBound#of} to specify a stream ID. - *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
      • Use {@link IdBound#of} to specify a stream entry ID. + *
      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
      * * @param count Maximum count of stream entries to return. - * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @return A Map of key to stream entry data, where entry data is an array of + * pairings with format [[field, entry], [field, entry], ...]. Returns or + * null if count is non-positive. * @example *
      {@code
            * // Retrieve the first 2 stream entries
            * Map result = client.xrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
            * result.forEach((k, v) -> {
      -     *     System.out.println("Stream ID: " + k);
      +     *     System.out.println("stream entry ID: " + k);
            *     for (int i = 0; i < v.length; i++) {
            *         System.out.println(v[i][0] + ": " + v[i][1]);
            *     }
      @@ -970,12 +994,13 @@ CompletableFuture xgroupSetId(
            *     Use the special ID of {@literal ">"} to receive only new messages.
            * @param group The consumer group name.
            * @param consumer The consumer name.
      -     * @return A {@literal Map>} with stream
      -     *      keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...].
      -     *      Returns null if there is no stream that can be served.
      +     * @return A {@literal Map>} with stream keys, to
      +     *     Map of stream entry IDs, to an array of pairings with format 
      +     *     [[field, entry], [field, entry], ...]. Returns null if there is no
      +     *     stream that can be served.
            * @example
            *     
      {@code
      -     * // create a new stream at "mystream", with stream id "1-0"
      +     * // create a new stream at "mystream", with stream entry ID "1-0"
            * String streamId = client.xadd("mystream", Map.of("myfield", "mydata"), StreamAddOptions.builder().id("1-0").build()).get();
            * assert client.xgroupCreate("mystream", "mygroup", "0-0").get().equals("OK"); // create the consumer group "mygroup"
            * Map> streamReadResponse = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "myconsumer").get();
      @@ -984,11 +1009,11 @@ CompletableFuture xgroupSetId(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
            * }
      -     * 
      + * }
      */ CompletableFuture>> xreadgroup( Map keysAndIds, String group, String consumer); @@ -1003,12 +1028,14 @@ CompletableFuture>> xreadgroup( * Use the special ID of {@literal gs(">")} to receive only new messages. * @param group The consumer group name. * @param consumer The consumer name. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. - * Returns null if there is no stream that can be served. + * @return A {@literal Map>} with + * stream keys, to Map of stream entry IDs, to an array of pairings with format + * + * [[field, entry], [field, entry], ...]. Returns null if there is no + * stream that can be served. * @example *
      {@code
      -     * // create a new stream at gs("mystream"), with stream id gs("1-0")
      +     * // create a new stream at gs("mystream"), with stream entry ID gs("1-0")
            * String streamId = client.xadd(gs("mystream"), Map.of(gs("myfield"), gs("mydata")), StreamAddOptionsBinary.builder().id(gs("1-0")).build()).get();
            * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("0-0")).get().equals("OK"); // create the consumer group gs("mygroup")
            * Map> streamReadResponse = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("myconsumer")).get();
      @@ -1017,11 +1044,11 @@ CompletableFuture>> xreadgroup(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
            * }
      -     * 
      + * }
      */ CompletableFuture>> xreadgroup( Map keysAndIds, GlideString group, GlideString consumer); @@ -1037,12 +1064,13 @@ CompletableFuture>> xreadgrou * @param group The consumer group name. * @param consumer The consumer name. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. - * Returns null if there is no stream that can be served. + * @return A {@literal Map>} with stream keys, to + * Map of stream entry IDs, to an array of pairings with format + * [[field, entry], [field, entry], ...]. Returns null if there is no + * stream that can be served. * @example *
      {@code
      -     * // create a new stream at "mystream", with stream id "1-0"
      +     * // create a new stream at "mystream", with stream entry ID "1-0"
            * String streamId = client.xadd("mystream", Map.of("myfield", "mydata"), StreamAddOptions.builder().id("1-0").build()).get();
            * assert client.xgroupCreate("mystream", "mygroup", "0-0").get().equals("OK"); // create the consumer group "mygroup"
            * StreamReadGroupOptions options = StreamReadGroupOptions.builder().count(1).build(); // retrieves only a single message at a time
      @@ -1052,11 +1080,11 @@ CompletableFuture>> xreadgrou
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
            * }
      -     * 
      + * }
      */ CompletableFuture>> xreadgroup( Map keysAndIds, @@ -1075,12 +1103,14 @@ CompletableFuture>> xreadgroup( * @param group The consumer group name. * @param consumer The consumer name. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. - * @return A {@literal Map>} with stream - * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. - * Returns null if there is no stream that can be served. + * @return A {@literal Map>} with + * stream keys, to Map of stream entry IDs, to an array of pairings with format + * + * [[field, entry], [field, entry], ...]. Returns null if there is no + * stream that can be served. * @example *
      {@code
      -     * // create a new stream at gs("mystream"), with stream id gs("1-0")
      +     * // create a new stream at gs("mystream"), with stream entry ID gs("1-0")
            * String streamId = client.xadd(gs("mystream"), Map.of(gs("myfield"), gs("mydata")), StreamAddOptionsBinary.builder().id(gs("1-0")).build()).get();
            * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("0-0")).get().equals("OK"); // create the consumer group gs("mygroup")
            * StreamReadGroupOptions options = StreamReadGroupOptions.builder().count(1).build(); // retrieves only a single message at a time
      @@ -1090,11 +1120,11 @@ CompletableFuture>> xreadgroup(
            *     System.out.printf("Key: %s", keyEntry.getKey());
            *     for (var streamEntry : keyEntry.getValue().entrySet()) {
            *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
      -     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
      +     *             System.out.printf("stream entry ID: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
            *         );
            *     }
            * }
      -     * 
      + * }
      */ CompletableFuture>> xreadgroup( Map keysAndIds, @@ -1103,8 +1133,9 @@ CompletableFuture>> xreadgrou StreamReadGroupOptions options); /** - * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. - * This command should be called on a pending message so that such message does not get processed again. + * Returns the number of messages that were successfully acknowledged by the consumer group member + * of a stream. This command should be called on a pending message so that such message does not + * get processed again. * * @see valkey.io for details. * @param key The key of the stream. @@ -1118,13 +1149,14 @@ CompletableFuture>> xreadgrou * var readResult = client.xreadgroup(Map.of("mystream", entryId), "mygroup", "my0consumer").get(); * // acknowledge messages on stream * assert 1L == client.xack("mystream", "mygroup", new String[] {entryId}).get(); - *
+ * }
*/ CompletableFuture xack(String key, String group, String[] ids); /** - * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. - * This command should be called on a pending message so that such message does not get processed again. + * Returns the number of messages that were successfully acknowledged by the consumer group member + * of a stream. This command should be called on a pending message so that such message does not + * get processed again. * * @param key The key of the stream. * @param group The consumer group name. @@ -1137,7 +1169,7 @@ CompletableFuture>> xreadgrou * var readResult = client.xreadgroup(Map.of(gs("mystream"), entryId), gs("mygroup"), gs("my0consumer")).get(); * // acknowledge messages on stream * assert 1L == client.xack(gs("mystream"), gs("mygroup"), new GlideString[] {entryId}).get(); - *
+ * }
*/ CompletableFuture xack(GlideString key, GlideString group, GlideString[] ids); @@ -1148,22 +1180,26 @@ CompletableFuture>> xreadgrou * @param key The key of the stream. * @param group The consumer group name. * @return An array that includes the summary of pending messages, with the format - * [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where: - *
    - *
  • NumOfMessages: The total number of pending messages for this consumer group. - *
  • StartId: The smallest ID among the pending messages. - *
  • EndId: The greatest ID among the pending messages. - *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every consumer - * in the consumer group with at least one pending message, and the number of pending messages it has. - *
- * @example - *
{@code
+     *     [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where:
+     *     
    + *
  • NumOfMessages: The total number of pending messages for this consumer + * group. + *
  • StartId: The smallest ID among the pending messages. + *
  • EndId: The greatest ID among the pending messages. + *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every + * consumer in the consumer group with at least one pending message, and the number of + * pending messages it has. + *
+ * + * @example + *
{@code
      * // Retrieve a summary of all pending messages from key "my_stream"
      * Object[] result = client.xpending("my_stream", "my_group").get();
      * System.out.println("Number of pending messages: " + result[0]);
      * System.out.println("Start and End ID of messages: [" + result[1] + ", " + result[2] + "]");
      * for (Object[] consumerResult : (Object[][]) result[3]) {
      *     System.out.println("Number of Consumer messages: [" + consumerResult[0] + ", " + consumerResult[1] + "]");
+     * }
      * }
*/ CompletableFuture xpending(String key, String group); @@ -1175,135 +1211,156 @@ CompletableFuture>> xreadgrou * @param key The key of the stream. * @param group The consumer group name. * @return An array that includes the summary of pending messages, with the format - * [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where: - *
    - *
  • NumOfMessages: The total number of pending messages for this consumer group. - *
  • StartId: The smallest ID among the pending messages. - *
  • EndId: The greatest ID among the pending messages. - *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every consumer - * in the consumer group with at least one pending message, and the number of pending messages it has. - *
- * @example - *
{@code
+     *     [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where:
+     *     
    + *
  • NumOfMessages: The total number of pending messages for this consumer + * group. + *
  • StartId: The smallest ID among the pending messages. + *
  • EndId: The greatest ID among the pending messages. + *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every + * consumer in the consumer group with at least one pending message, and the number of + * pending messages it has. + *
+ * + * @example + *
{@code
      * // Retrieve a summary of all pending messages from key "my_stream"
      * Object[] result = client.xpending(gs("my_stream"), gs("my_group")).get();
      * System.out.println("Number of pending messages: " + result[0]);
      * System.out.println("Start and End ID of messages: [" + result[1] + ", " + result[2] + "]");
      * for (Object[] consumerResult : (Object[][]) result[3]) {
      *     System.out.println("Number of Consumer messages: [" + consumerResult[0] + ", " + consumerResult[1] + "]");
+     * }
      * }
*/ CompletableFuture xpending(GlideString key, GlideString group); /** - * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. * * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
* - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
+ * * @param count Limits the number of messages returned. - * @return A 2D-array of 4-tuples containing extended message information with the format - * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: - *
    - *
  • ID: The ID of the message. - *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. - *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - *
  • NumOfDelivered: The number of times this message was delivered. - *
- * @example - *
{@code
+     * @return A 2D-array of 4-tuples containing extended message information with the
+     *     format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where:
+     *     
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * + * @example + *
{@code
      * // Retrieve up to 10 pending messages from key "my_stream" in extended form
      * Object[][] result = client.xpending("my_stream", "my_group", InfRangeBound.MIN, InfRangeBound.MAX, 10L).get();
      * for (Object[] messageResult : result) {
      *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
      * }
*/ CompletableFuture xpending( String key, String group, StreamRange start, StreamRange end, long count); /** - * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. * * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
* - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
+ * * @param count Limits the number of messages returned. - * @return A 2D-array of 4-tuples containing extended message information with the format - * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: - *
    - *
  • ID: The ID of the message. - *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. - *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - *
  • NumOfDelivered: The number of times this message was delivered. - *
- * @example - *
{@code
+     * @return A 2D-array of 4-tuples containing extended message information with the
+     *     format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where:
+     *     
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * + * @example + *
{@code
      * // Retrieve up to 10 pending messages from key "my_stream" in extended form
      * Object[][] result = client.xpending(gs("my_stream"), gs("my_group"), InfRangeBound.MIN, InfRangeBound.MAX, 10L).get();
      * for (Object[] messageResult : result) {
      *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
      * }
*/ CompletableFuture xpending( GlideString key, GlideString group, StreamRange start, StreamRange end, long count); /** - * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. * * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
* - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
+ * * @param count Limits the number of messages returned. * @param options Stream add options {@link StreamPendingOptions}. - * @return A 2D-array of 4-tuples containing extended message information with the format - * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: - *
    - *
  • ID: The ID of the message. - *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. - *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - *
  • NumOfDelivered: The number of times this message was delivered. - *
- * @example - *
{@code
+     * @return A 2D-array of 4-tuples containing extended message information with the
+     *     format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where:
+     *     
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * + * @example + *
{@code
      * // Retrieve up to 10 pending messages from key "my_stream" and consumer "my_consumer" in extended form
      * Object[][] result = client.xpending(
      *     "my_stream",
@@ -1315,6 +1372,7 @@ CompletableFuture xpending(
      * ).get();
      * for (Object[] messageResult : result) {
      *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
      * }
*/ CompletableFuture xpending( @@ -1326,36 +1384,41 @@ CompletableFuture xpending( StreamPendingOptions options); /** - * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. * * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
* - * @param end Ending stream ID bound for range. + * @param end Ending stream entry ID bound for range. *
    - *
  • Use {@link IdBound#of} to specify a stream ID. - *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link IdBound#of} to specify a stream entry ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry ID. *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
+ * * @param count Limits the number of messages returned. * @param options Stream add options {@link StreamPendingOptionsBinary}. - * @return A 2D-array of 4-tuples containing extended message information with the format - * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: - *
    - *
  • ID: The ID of the message. - *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. - *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - *
  • NumOfDelivered: The number of times this message was delivered. - *
- * @example - *
{@code
+     * @return A 2D-array of 4-tuples containing extended message information with the
+     *     format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where:
+     *     
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * + * @example + *
{@code
      * // Retrieve up to 10 pending messages from key "my_stream" and consumer "my_consumer" in extended form
      * Object[][] result = client.xpending(
      *     gs("my_stream"),
@@ -1367,6 +1430,7 @@ CompletableFuture xpending(
      * ).get();
      * for (Object[] messageResult : result) {
      *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
      * }
*/ CompletableFuture xpending( @@ -1389,7 +1453,7 @@ CompletableFuture xpending( * @return A Map of message entries with the format * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. * @example - *
+     *     
{@code
      * // read messages from streamId for consumer1
      * var readResult = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "consumer1").get();
      * // "entryId" is now read, and we can assign the pending messages to consumer2
@@ -1400,7 +1464,7 @@ CompletableFuture xpending(
      *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
      *     }
      * }
-     * 
+ * }
*/ CompletableFuture> xclaim( String key, String group, String consumer, long minIdleTime, String[] ids); @@ -1417,7 +1481,7 @@ CompletableFuture> xclaim( * @return A Map of message entries with the format * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. * @example - *
+     *     
{@code
      * // read messages from streamId for consumer1
      * var readResult = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("consumer1")).get();
      * // "entryId" is now read, and we can assign the pending messages to consumer2
@@ -1428,7 +1492,7 @@ CompletableFuture> xclaim(
      *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
      *     }
      * }
-     * 
+ * }
*/ CompletableFuture> xclaim( GlideString key, @@ -1450,7 +1514,7 @@ CompletableFuture> xclaim( * @return A Map of message entries with the format * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. * @example - *
+     *     
{@code
      * // assign (force) unread and unclaimed messages to consumer2
      * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
      * Map results = client.xclaim("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}, options).get();
@@ -1460,7 +1524,7 @@ CompletableFuture> xclaim(
      *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
      *     }
      * }
-     * 
+ * }
*/ CompletableFuture> xclaim( String key, @@ -1483,7 +1547,7 @@ CompletableFuture> xclaim( * @return A Map of message entries with the format * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. * @example - *
+     *     
{@code
      * // assign (force) unread and unclaimed messages to consumer2
      * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
      * Map results = client.xclaim(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}, options).get();
@@ -1493,7 +1557,7 @@ CompletableFuture> xclaim(
      *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
      *     }
      * }
-     * 
+ * }
*/ CompletableFuture> xclaim( GlideString key, @@ -1515,7 +1579,7 @@ CompletableFuture> xclaim( * @param ids An array of entry ids. * @return An array of message ids claimed by the consumer. * @example - *
+     *     
{@code
      * // read messages from streamId for consumer1
      * var readResult = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "consumer1").get();
      * // "entryId" is now read, and we can assign the pending messages to consumer2
@@ -1523,7 +1587,7 @@ CompletableFuture> xclaim(
      * for (String id: results) {
      *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
      * }
-     * 
+ * }
*/ CompletableFuture xclaimJustId( String key, String group, String consumer, long minIdleTime, String[] ids); @@ -1540,7 +1604,7 @@ CompletableFuture xclaimJustId( * @param ids An array of entry ids. * @return An array of message ids claimed by the consumer. * @example - *
+     *     
{@code
      * // read messages from streamId for consumer1
      * var readResult = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("consumer1")).get();
      * // "entryId" is now read, and we can assign the pending messages to consumer2
@@ -1548,7 +1612,7 @@ CompletableFuture xclaimJustId(
      * for (GlideString id: results) {
      *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
      * }
-     * 
+ * }
*/ CompletableFuture xclaimJustId( GlideString key, @@ -1570,13 +1634,14 @@ CompletableFuture xclaimJustId( * @param options Stream claim options {@link StreamClaimOptions}. * @return An array of message ids claimed by the consumer. * @example - *
+     *     
{@code
      * // assign (force) unread and unclaimed messages to consumer2
      * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
      * String[] results = client.xclaimJustId("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}, options).get();
      * for (String id: results) {
      *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
      * }
+     * }
*/ CompletableFuture xclaimJustId( String key, @@ -1599,13 +1664,14 @@ CompletableFuture xclaimJustId( * @param options Stream claim options {@link StreamClaimOptions}. * @return An array of message ids claimed by the consumer. * @example - *
+     *     
{@code
      * // assign (force) unread and unclaimed messages to consumer2
      * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
      * GlideString[] results = client.xclaimJustId(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}, options).get();
      * for (GlideString id: results) {
      *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
      * }
+     * }
*/ CompletableFuture xclaimJustId( GlideString key, @@ -1722,7 +1788,8 @@ CompletableFuture[]> xinfoConsumers( * specified value. * @return An array containing the following elements: *
    - *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
  • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -1756,7 +1823,8 @@ CompletableFuture xautoclaim( * specified value. * @return An array containing the following elements: *
      - *
    • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
    • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
    • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -1795,7 +1863,8 @@ CompletableFuture xautoclaim( * @param count Limits the number of claimed entries to the specified value. * @return An array containing the following elements: *
        - *
      • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
      • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
      • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -1830,7 +1899,8 @@ CompletableFuture xautoclaim( * @param count Limits the number of claimed entries to the specified value. * @return An array containing the following elements: *
          - *
        • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
        • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
        • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -1871,7 +1941,8 @@ CompletableFuture xautoclaim( * specified value. * @return An array containing the following elements: *
            - *
          • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
          • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
          • A list of the IDs for the claimed entries. @@ -1905,7 +1976,8 @@ CompletableFuture xautoclaimJustId( * specified value. * @return An array containing the following elements: *
              - *
            • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
            • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
            • A list of the IDs for the claimed entries. @@ -1944,7 +2016,8 @@ CompletableFuture xautoclaimJustId( * @param count Limits the number of claimed entries to the specified value. * @return An array containing the following elements: *
                - *
              • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
              • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
              • A list of the IDs for the claimed entries. @@ -1979,7 +2052,8 @@ CompletableFuture xautoclaimJustId( * @param count Limits the number of claimed entries to the specified value. * @return An array containing the following elements: *
                  - *
                • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
                • A stream entry ID to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
                • A list of the IDs for the claimed entries. diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 868e49206f..94a2ab59a8 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -3444,9 +3444,8 @@ public T xadd( * @see valkey.io for details. * @param keysAndIds A Map of keys and entry IDs to read from. * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of - * pairings with format [[field, entry], - * [field, entry], ...]. + * String[][]>>} with stream keys, to Map of stream entry IDs, to an array + * of pairings with format [[field, entry], [field, entry], ...]. */ public T xread(@NonNull Map keysAndIds) { return xread(keysAndIds, StreamReadOptions.builder().build()); @@ -3461,9 +3460,8 @@ public T xread(@NonNull Map keysAndIds) { * @param keysAndIds A Map of keys and entry IDs to read from. * @param options options detailing how to read the stream {@link StreamReadOptions}. * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of - * pairings with format [[field, entry], - * [field, entry], ...]. + * String[][]>>} with stream keys, to Map of stream entry IDs, to an array + * of pairings with format [[field, entry], [field, entry], ...]. */ public T xread( @NonNull Map keysAndIds, @NonNull StreamReadOptions options) { @@ -3535,25 +3533,25 @@ public T xdel(@NonNull ArgType key, @NonNull ArgType[] ids) { * will throw {@link IllegalArgumentException}. * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                    - *
                  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                  • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. *
                  * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                    - *
                  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                  • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. *
                  * * @return Command Response - A Map of key to stream entry data, where entry data is - * an array of pairings with format [[field, - * entry], [field, entry], ...]. + * an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns or null if count is non-positive. */ public T xrange( @NonNull ArgType key, @NonNull StreamRange start, @NonNull StreamRange end) { @@ -3570,17 +3568,17 @@ public T xrange( * will throw {@link IllegalArgumentException}. * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                    - *
                  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                  • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. *
                  * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                    - *
                  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                  • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. @@ -3588,8 +3586,8 @@ public T xrange( * * @param count Maximum count of stream entries to return. * @return Command Response - A Map of key to stream entry data, where entry data is - * an array of pairings with format [[field, - * entry], [field, entry], ...]. + * an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns or null if count is non-positive. */ public T xrange( @NonNull ArgType key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { @@ -3608,25 +3606,25 @@ public T xrange( * will throw {@link IllegalArgumentException}. * @see valkey.io for details. * @param key The key of the stream. - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                      - *
                    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                    • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                    • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. *
                    * - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                      - *
                    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                    • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                    • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. *
                    * * @return Command Response - A Map of key to stream entry data, where entry data is - * an array of pairings with format [[field, - * entry], [field, entry], ...]. + * an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns or null if count is non-positive. */ public T xrevrange( @NonNull ArgType key, @NonNull StreamRange end, @NonNull StreamRange start) { @@ -3645,17 +3643,17 @@ public T xrevrange( * will throw {@link IllegalArgumentException}. * @see valkey.io for details. * @param key The key of the stream. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                      - *
                    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                    • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                    • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. *
                    * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                      - *
                    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
                    • Use {@link StreamRange.IdBound#of} to specify a stream entry IDs. *
                    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream * ID. *
                    • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. @@ -3664,6 +3662,7 @@ public T xrevrange( * @param count Maximum count of stream entries to return. * @return Command Response - A Map of key to stream entry data, where entry data is * an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns or null if count is non-positive. */ public T xrevrange( @NonNull ArgType key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { @@ -3842,10 +3841,10 @@ public T xgroupSetId( * @param group The consumer group name. * @param consumer The newly created consumer. * @return Command Response - A {@literal Map>} with - * stream keys, to Map of stream-ids, to an array of pairings with format - * [[field, entry], [field, entry], ...]. Returns null if the consumer - * group does not exist. Returns a Map with a value of code>null if the - * stream is empty. + * stream keys, to Map of stream entry IDs, to an array of pairings with format + * + * [[field, entry], [field, entry], ...]. Returns null if there is no + * stream that can be served. */ public T xreadgroup( @NonNull Map keysAndIds, @@ -3867,10 +3866,10 @@ public T xreadgroup( * @param consumer The newly created consumer. * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. * @return Command Response - A {@literal Map>} with - * stream keys, to Map of stream-ids, to an array of pairings with format - * [[field, entry], [field, entry], ...]. Returns null if the consumer - * group does not exist. Returns a Map with a value of code>null if the - * stream is empty. + * stream keys, to Map of stream entry IDs, to an array of pairings with format + * + * [[field, entry], [field, entry], ...]. Returns null if there is no + * stream that can be served. */ public T xreadgroup( @NonNull Map keysAndIds, @@ -3942,17 +3941,17 @@ public T xpending(@NonNull ArgType key, @NonNull ArgType group) { * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                        - *
                      • Use {@link IdBound#of} to specify a stream ID. - *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
                      • Use {@link IdBound#of} to specify a stream entry IDs. + *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry IDs. *
                      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
                      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                        - *
                      • Use {@link IdBound#of} to specify a stream ID. - *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
                      • Use {@link IdBound#of} to specify a stream entry IDs. + *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry IDs. *
                      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
                      * @@ -3988,17 +3987,17 @@ public T xpending( * @see valkey.io for details. * @param key The key of the stream. * @param group The consumer group name. - * @param start Starting stream ID bound for range. + * @param start Starting stream entry IDs bound for range. *
                        - *
                      • Use {@link IdBound#of} to specify a stream ID. - *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
                      • Use {@link IdBound#of} to specify a stream entry IDs. + *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry IDs. *
                      • Use {@link InfRangeBound#MIN} to start with the minimum available ID. *
                      * - * @param end Ending stream ID bound for range. + * @param end Ending stream entry IDs bound for range. *
                        - *
                      • Use {@link IdBound#of} to specify a stream ID. - *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
                      • Use {@link IdBound#of} to specify a stream entry IDs. + *
                      • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream entry IDs. *
                      • Use {@link InfRangeBound#MAX} to end with the maximum available ID. *
                      * @@ -4262,7 +4261,8 @@ public T xinfoConsumers(@NonNull ArgType key, @NonNull ArgType groupNa * specified value. * @return Command Response - An array containing the following elements: *
                        - *
                      • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
                      • A stream entry IDs to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
                      • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -4301,7 +4301,8 @@ public T xautoclaim( * @param count Limits the number of claimed entries to the specified value. * @return Command Response - An array containing the following elements: *
                          - *
                        • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
                        • A stream entry IDs to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
                        • A mapping of the claimed entries, with the keys being the claimed entry IDs and the @@ -4349,7 +4350,8 @@ public T xautoclaim( * specified value. * @return Command Response - An array containing the following elements: *
                            - *
                          • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
                          • A stream entry IDs to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
                          • A list of the IDs for the claimed entries. @@ -4394,7 +4396,8 @@ public T xautoclaimJustId( * @param count Limits the number of claimed entries to the specified value. * @return Command Response - An array containing the following elements: *
                              - *
                            • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + *
                            • A stream entry IDs to be used as the start argument for the next call to + * XAUTOCLAIM * . This ID is equivalent to the next ID in the stream after the entries that * were scanned, or "0-0" if the entire stream was scanned. *
                            • A list of the IDs for the claimed entries. From 59b3e8b283bda65be8868ae85df78154fe7a649f Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Fri, 23 Aug 2024 11:22:59 -0700 Subject: [PATCH 215/236] lmpop and lpushx command added (#2191) Signed-off-by: Prateek Kumar --- node/src/BaseClient.ts | 7 ++++-- node/src/Commands.ts | 8 +++--- node/src/Transaction.ts | 8 ++++-- node/tests/SharedTests.ts | 53 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 117f5ccaa7..1e52cbcb8b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2062,7 +2062,10 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that the list has two elements. * ``` */ - public async lpushx(key: string, elements: string[]): Promise { + public async lpushx( + key: GlideString, + elements: GlideString[], + ): Promise { return this.createWritePromise(createLPushX(key, elements)); } @@ -6437,7 +6440,7 @@ export class BaseClient { * ``` */ public async lmpop( - keys: string[], + keys: GlideString[], direction: ListDirection, count?: number, ): Promise> { diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d99174cf70..01080ca175 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -866,8 +866,8 @@ export function createLPush( * @internal */ export function createLPushX( - key: string, - elements: string[], + key: GlideString, + elements: GlideString[], ): command_request.Command { return createCommand(RequestType.LPushX, [key].concat(elements)); } @@ -3826,11 +3826,11 @@ export function createAppend( * @internal */ export function createLMPop( - keys: string[], + keys: GlideString[], direction: ListDirection, count?: number, ): command_request.Command { - const args: string[] = [keys.length.toString(), ...keys, direction]; + const args: GlideString[] = [keys.length.toString(), ...keys, direction]; if (count !== undefined) { args.push("COUNT"); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b2b53bf589..8f36c4dfcb 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1041,7 +1041,7 @@ export class BaseTransaction> { * * Command Response - The length of the list after the push operation. */ - public lpushx(key: string, elements: string[]): T { + public lpushx(key: GlideString, elements: GlideString[]): T { return this.addAndReturn(createLPushX(key, elements)); } @@ -3757,7 +3757,11 @@ export class BaseTransaction> { * * Command Response - A `Record` of `key` name mapped array of popped elements. */ - public lmpop(keys: string[], direction: ListDirection, count?: number): T { + public lmpop( + keys: GlideString[], + direction: ListDirection, + count?: number, + ): T { return this.addAndReturn(createLMPop(keys, direction, count)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index e3073d33ee..d930fb9acc 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2080,6 +2080,23 @@ export function runBaseTests(config: { await expect(client.lpushx(key2, [])).rejects.toThrow( RequestError, ); + + // test for binary key as input + const key4 = uuidv4(); + expect(await client.lpush(key4, ["0"])).toEqual(1); + expect( + await client.lpushx(Buffer.from(key4), [ + Buffer.from("1"), + Buffer.from("2"), + Buffer.from("3"), + ]), + ).toEqual(4); + expect(await client.lrange(key4, 0, -1)).toEqual([ + "3", + "2", + "1", + "0", + ]); }, protocol); }, config.timeout, @@ -10400,6 +10417,42 @@ export function runBaseTests(config: { await expect( client.lmpop([nonListKey], ListDirection.RIGHT), ).rejects.toThrow(RequestError); + + // Test with single binary key array as input + const key3 = "{key}" + uuidv4(); + const singleKeyArrayWithKey3 = [Buffer.from(key3)]; + + // pushing to the arrays to be popped + expect(await client.lpush(key3, lpushArgs)).toEqual(5); + const expectedWithKey3 = { [key3]: ["five"] }; + + // checking correct result from popping + expect( + await client.lmpop( + singleKeyArrayWithKey3, + ListDirection.LEFT, + ), + ).toEqual(expectedWithKey3); + + // test with multiple binary keys array as input + const key4 = "{key}" + uuidv4(); + const multiKeyArrayWithKey3AndKey4 = [ + Buffer.from(key4), + Buffer.from(key3), + ]; + + // pushing to the arrays to be popped + expect(await client.lpush(key4, lpushArgs)).toEqual(5); + const expectedWithKey4 = { [key4]: ["one", "two"] }; + + // checking correct result from popping + expect( + await client.lmpop( + multiKeyArrayWithKey3AndKey4, + ListDirection.RIGHT, + 2, + ), + ).toEqual(expectedWithKey4); }, protocol); }, config.timeout, From 68deaab0552212f58020e290b5d50aa127b44097 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 23 Aug 2024 12:31:17 -0700 Subject: [PATCH 216/236] Node: Add binary variant to connection management commands. (#2160) * Add binary variant to connection management commands. Signed-off-by: Yury-Fridlyand Co-authored-by: Yi-Pin Chen Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 8 +- node/src/Commands.ts | 4 +- node/src/GlideClient.ts | 82 ++++++++++++------ node/src/GlideClusterClient.ts | 114 ++++++++++++++++---------- node/src/Transaction.ts | 36 +++++--- node/tests/GlideClient.test.ts | 4 +- node/tests/GlideClusterClient.test.ts | 10 ++- node/tests/SharedTests.ts | 50 ++++++++++- 9 files changed, 215 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbe8db8f3..a7296b9e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added/updated binary variant to connection management commands and WATCH/UNWATCH ([#2160](https://github.com/valkey-io/valkey-glide/pull/2160)) * Java: Fix docs for stream commands ([#2086](https://github.com/valkey-io/valkey-glide/pull/2086)) * Node: Added binary variant to bitmap commands ([#2178](https://github.com/valkey-io/valkey-glide/pull/2178)) * Node: Added binary variant to generic commands ([#2158](https://github.com/valkey-io/valkey-glide/pull/2158)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1e52cbcb8b..16d23e62e9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -6320,7 +6320,7 @@ export class BaseClient { * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. * * @param keys - The keys to watch. - * @returns A simple "OK" response. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -6339,8 +6339,10 @@ export class BaseClient { * console.log(result); // Output: null - null is returned when the watched key is modified before transaction execution. * ``` */ - public async watch(keys: string[]): Promise<"OK"> { - return this.createWritePromise(createWatch(keys)); + public async watch(keys: GlideString[]): Promise<"OK"> { + return this.createWritePromise(createWatch(keys), { + decoder: Decoder.String, + }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 01080ca175..0b7aeb3c73 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1933,7 +1933,7 @@ export function createZPopMax( /** * @internal */ -export function createEcho(message: string): command_request.Command { +export function createEcho(message: GlideString): command_request.Command { return createCommand(RequestType.Echo, [message]); } @@ -3729,7 +3729,7 @@ export function createRandomKey(): command_request.Command { } /** @internal */ -export function createWatch(keys: string[]): command_request.Command { +export function createWatch(keys: GlideString[]): command_request.Command { return createCommand(RequestType.Watch, keys); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 568889849b..ac2ed3f1ec 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -224,15 +224,17 @@ export class GlideClient extends BaseClient { }); } - /** Ping the Redis server. + /** + * Pings the server. + * * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * - * @param message - An optional message to include in the PING command. - * If not provided, the server will respond with "PONG". - * If provided, the server will respond with a copy of the message. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. - * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. - * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. + * @param options - (Optional) Additional parameters: + * - (Optional) `message` : a message to include in the `PING` command. + * + If not provided, the server will respond with `"PONG"`. + * + If provided, the server will respond with a copy of the message. + * - (Optional) `decoder`: see {@link DecoderOption}. + * @returns `"PONG"` if `message` is not provided, otherwise return a copy of `message`. * * @example * ```typescript @@ -248,10 +250,11 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public async ping(options?: { - message?: GlideString; - decoder?: Decoder; - }): Promise { + public async ping( + options?: { + message?: GlideString; + } & DecoderOption, + ): Promise { return this.createWritePromise(createPing(options?.message), { decoder: options?.decoder, }); @@ -268,11 +271,13 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createInfo(options)); } - /** Change the currently selected Redis database. + /** + * Changes the currently selected database. + * * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. - * @returns A simple OK response. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -282,13 +287,19 @@ export class GlideClient extends BaseClient { * ``` */ public async select(index: number): Promise<"OK"> { - return this.createWritePromise(createSelect(index)); + return this.createWritePromise(createSelect(index), { + decoder: Decoder.String, + }); } - /** Get the name of the primary's connection. + /** + * Gets the name of the primary's connection. + * * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for more details. * - * @returns the name of the client connection as a string if a name is set, or null if no name is assigned. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. + * @returns The name of the client connection as a string if a name is set, or `null` if no name is assigned. * * @example * ```typescript @@ -297,8 +308,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'Client Name' * ``` */ - public async clientGetName(): Promise { - return this.createWritePromise(createClientGetName()); + public async clientGetName(decoder?: Decoder): Promise { + return this.createWritePromise(createClientGetName(), { + decoder: decoder, + }); } /** Rewrite the configuration file with the current configuration. @@ -334,10 +347,18 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createConfigResetStat()); } - /** Returns the current connection id. + /** + * Returns the current connection ID. + * * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * - * @returns the id of the client. + * @returns The ID of the connection. + * + * @example + * ```typescript + * const result = await client.clientId(); + * console.log("Connection id: " + result); + * ``` */ public async clientId(): Promise { return this.createWritePromise(createClientId()); @@ -382,10 +403,14 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createConfigSet(parameters)); } - /** Echoes the provided `message` back. + /** + * Echoes the provided `message` back. + * * @see {@link https://valkey.io/commands/echo|valkey.io} for more details. * * @param message - The message to be echoed back. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The provided `message`. * * @example @@ -395,8 +420,13 @@ export class GlideClient extends BaseClient { * console.log(echoedMessage); // Output: 'valkey-glide' * ``` */ - public async echo(message: string): Promise { - return this.createWritePromise(createEcho(message)); + public async echo( + message: GlideString, + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createEcho(message), { + decoder, + }); } /** Returns the server time @@ -923,7 +953,7 @@ export class GlideClient extends BaseClient { * * @see {@link https://valkey.io/commands/unwatch/|valkey.io} and {@link https://valkey.io/topics/transactions/#cas|Valkey Glide Wiki} for more details. * - * @returns A simple "OK" response. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -934,6 +964,8 @@ export class GlideClient extends BaseClient { * ``` */ public async unwatch(): Promise<"OK"> { - return this.createWritePromise(createUnWatch()); + return this.createWritePromise(createUnWatch(), { + decoder: Decoder.String, + }); } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 1cb8ac42c0..ed3492cd3d 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -408,18 +408,20 @@ export class GlideClusterClient extends BaseClient { ); } - /** Ping the Redis server. + /** + * Pings the server. + * + * The command will be routed to all primary nodes, unless `route` is provided. * * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * - * @param message - An optional message to include in the PING command. - * If not provided, the server will respond with "PONG". - * If provided, the server will respond with a copy of the message. - * @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`. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. - * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. - * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. + * @param options - (Optional) Additional parameters: + * - (Optional) `message` : a message to include in the `PING` command. + * + If not provided, the server will respond with `"PONG"`. + * + If provided, the server will respond with a copy of the message. + * - (Optional) `route`: see {@link RouteOption}. + * - (Optional) `decoder`: see {@link DecoderOption}. + * @returns `"PONG"` if `message` is not provided, otherwise return a copy of `message`. * * @example * ```typescript @@ -435,11 +437,12 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'Hello' * ``` */ - public async ping(options?: { - message?: GlideString; - route?: Routes; - decoder?: Decoder; - }): Promise { + public async ping( + options?: { + message?: GlideString; + } & RouteOption & + DecoderOption, + ): Promise { return this.createWritePromise(createPing(options?.message), { route: toProtobufRoute(options?.route), decoder: options?.decoder, @@ -466,15 +469,18 @@ export class GlideClusterClient extends BaseClient { ); } - /** Get the name of the connection to which the request is routed. + /** + * Gets the name of the connection to which the request is routed. + * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for details. * - * @param route - The command will be routed a random node, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * - * @returns - the name of the client connection as a string if a name is set, or null if no name is assigned. - * When specifying a route other than a single node, it returns a dictionary where each address is the key and - * its corresponding node response is the value. + * @returns - The name of the client connection as a string if a name is set, or `null` if no name is assigned. + * When specifying a route other than a single node, it returns a dictionary where each address is the key and + * its corresponding node response is the value. * * @example * ```typescript @@ -491,11 +497,14 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async clientGetName( - route?: Routes, - ): Promise> { - return this.createWritePromise>( + options?: RouteOption & DecoderOption, + ): Promise> { + return this.createWritePromise>( createClientGetName(), - { route: toProtobufRoute(route) }, + { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, + }, ); } @@ -539,18 +548,29 @@ export class GlideClusterClient extends BaseClient { }); } - /** Returns the current connection id. + /** + * Returns the current connection ID. + * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * - * @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 the id of the client. When specifying a route other than a single node, - * it returns a dictionary where each address is the key and its corresponding node response is the value. + * @param options - (Optional) See {@link RouteOption}. + * @returns The ID of the connection. When specifying a route other than a single node, + * it returns a dictionary where each address is the key and its corresponding node response is the value. + * + * @example + * ```typescript + * const result = await client.clientId(); + * console.log("Connection id: " + result); + * ``` */ - public async clientId(route?: Routes): Promise> { + public async clientId( + options?: RouteOption, + ): Promise> { return this.createWritePromise>( createClientId(), - { route: toProtobufRoute(route) }, + { route: toProtobufRoute(options?.route) }, ); } @@ -614,14 +634,17 @@ export class GlideClusterClient extends BaseClient { }); } - /** Echoes the provided `message` back. + /** + * Echoes the provided `message` back. + * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/echo/|valkey.io} for details. * * @param message - The message to be echoed back. - * @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`. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * @returns The provided `message`. When specifying a route other than a single node, - * it returns a dictionary where each address is the key and its corresponding node response is the value. + * it returns a dictionary where each address is the key and its corresponding node response is the value. * * @example * ```typescript @@ -637,11 +660,12 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async echo( - message: string, - route?: Routes, - ): Promise> { + message: GlideString, + options?: RouteOption & DecoderOption, + ): Promise> { return this.createWritePromise(createEcho(message), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } @@ -1344,11 +1368,12 @@ export class GlideClusterClient extends BaseClient { * Flushes all the previously watched keys for a transaction. Executing a transaction will * automatically flush all previously watched keys. * + * The command will be routed to all primary nodes, unless `route` is provided + * * @see {@link https://valkey.io/commands/unwatch/|valkey.io} and {@link https://valkey.io/topics/transactions/#cas|Valkey Glide Wiki} for more details. * - * @param route - (Optional) The command will be routed to all primary nodes, unless `route` is provided, - * in which case the client will route the command to the nodes defined by `route`. - * @returns A simple "OK" response. + * @param options - (Optional) See {@link RouteOption}. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -1358,9 +1383,10 @@ export class GlideClusterClient extends BaseClient { * console.log(response); // Output: "OK" * ``` */ - public async unwatch(route?: Routes): Promise<"OK"> { + public async unwatch(options?: RouteOption): Promise<"OK"> { return this.createWritePromise(createUnWatch(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8f36c4dfcb..f4982236e1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -390,14 +390,16 @@ export class BaseTransaction> { return this.addAndReturn(createSet(key, value, options)); } - /** Ping the Redis server. + /** + * Pings the server. + * * @see {@link https://valkey.io/commands/ping/|valkey.io} for details. * - * @param message - An optional message to include in the PING command. - * If not provided, the server will respond with "PONG". - * If provided, the server will respond with a copy of the message. + * @param message - (Optional) A message to include in the PING command. + * - If not provided, the server will respond with `"PONG"`. + * - If provided, the server will respond with a copy of the message. * - * Command Response - "PONG" if `message` is not provided, otherwise return a copy of `message`. + * Command Response - `"PONG"` if `message` is not provided, otherwise return a copy of `message`. */ public ping(message?: GlideString): T { return this.addAndReturn(createPing(message)); @@ -465,10 +467,12 @@ export class BaseTransaction> { return this.addAndReturn(createRestore(key, ttl, value, options)); } - /** Get the name of the connection on which the transaction is being executed. + /** + * Gets the name of the connection on which the transaction is being executed. + * * @see {@link https://valkey.io/commands/client-getname/|valkey.io} for details. * - * Command Response - the name of the client connection as a string if a name is set, or null if no name is assigned. + * Command Response - The name of the client connection as a string if a name is set, or null if no name is assigned. */ public clientGetName(): T { return this.addAndReturn(createClientGetName()); @@ -566,10 +570,12 @@ export class BaseTransaction> { return this.addAndReturn(createIncrByFloat(key, amount)); } - /** Returns the current connection id. + /** + * Returns the current connection ID. + * * @see {@link https://valkey.io/commands/client-id/|valkey.io} for details. * - * Command Response - the id of the client. + * Command Response - The ID of the connection. */ public clientId(): T { return this.addAndReturn(createClientId()); @@ -2225,14 +2231,16 @@ export class BaseTransaction> { return this.addAndReturn(createBZPopMax(keys, timeout)); } - /** Echoes the provided `message` back. + /** + * Echoes the provided `message` back + * * @see {@link https://valkey.io/commands/echo/|valkey.io} for more details. * * @param message - The message to be echoed back. * * Command Response - The provided `message`. */ - public echo(message: string): T { + public echo(message: GlideString): T { return this.addAndReturn(createEcho(message)); } @@ -3860,12 +3868,14 @@ export class BaseTransaction> { export class Transaction extends BaseTransaction { /// TODO: add MOVE, SLAVEOF and all SENTINEL commands - /** Change the currently selected Redis database. + /** + * Change the currently selected database. + * * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. * - * Command Response - A simple OK response. + * Command Response - A simple `"OK"` response. */ public select(index: number): Transaction { return this.addAndReturn(createSelect(index)); diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 484eab350e..99f379e61c 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -1418,7 +1418,9 @@ describe("GlideClient", () => { expect(await client.get(key3)).toEqual("foobar"); // Transaction executes command successfully with unmodified watched keys - expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.watch([key1, Buffer.from(key2), key3])).toEqual( + "OK", + ); results = await client.exec(setFoobarTransaction); expect(results).toEqual(["OK", "OK", "OK"]); // sanity check diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 538ce8c45a..c1f7f4e389 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -342,7 +342,7 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const message = uuidv4(); - const echoDict = await client.echo(message, "allNodes"); + const echoDict = await client.echo(message, { route: "allNodes" }); expect(typeof echoDict).toBe("object"); expect(intoArray(echoDict)).toEqual( @@ -1675,7 +1675,9 @@ describe("GlideClusterClient", () => { // Transaction executes command successfully with a read command on the watch key before // transaction is executed. - expect(await client.watch([key1, key2, key3])).toEqual("OK"); + expect(await client.watch([key1, key2, Buffer.from(key3)])).toEqual( + "OK", + ); expect(await client.get(key2)).toEqual("hello"); results = await client.exec(setFoobarTransaction); expect(results).toEqual(["OK", "OK", "OK"]); @@ -1733,7 +1735,9 @@ describe("GlideClusterClient", () => { expect(await client.watch([key1, key2])).toEqual("OK"); expect(await client.set(key2, "hello")).toEqual("OK"); expect(await client.unwatch()).toEqual("OK"); - expect(await client.unwatch("allPrimaries")).toEqual("OK"); + expect(await client.unwatch({ route: "allPrimaries" })).toEqual( + "OK", + ); setFoobarTransaction.set(key1, "foobar").set(key2, "foobar"); const results = await client.exec(setFoobarTransaction); expect(results).toEqual(["OK", "OK"]); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d930fb9acc..97ae5107ba 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -162,7 +162,19 @@ export function runBaseTests(config: { async (protocol) => { await runTest( async (client: BaseClient) => { - expect(await client.clientGetName()).toBe("TEST_CLIENT"); + expect(await client.clientGetName()).toEqual("TEST_CLIENT"); + + if (client instanceof GlideClient) { + expect( + await client.clientGetName(Decoder.Bytes), + ).toEqual(Buffer.from("TEST_CLIENT")); + } else { + expect( + await client.clientGetName({ + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from("TEST_CLIENT")); + } }, protocol, { clientName: "TEST_CLIENT" }, @@ -492,11 +504,16 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const pongEncoded = Buffer.from("PONG"); - const helloEncoded = Buffer.from("Hello"); expect(await client.ping()).toEqual("PONG"); expect(await client.ping({ message: "Hello" })).toEqual( "Hello", ); + expect( + await client.ping({ + message: pongEncoded, + decoder: Decoder.String, + }), + ).toEqual("PONG"); expect(await client.ping({ decoder: Decoder.Bytes })).toEqual( pongEncoded, ); @@ -505,7 +522,7 @@ export function runBaseTests(config: { message: "Hello", decoder: Decoder.Bytes, }), - ).toEqual(helloEncoded); + ).toEqual(Buffer.from("Hello")); }, protocol); }, config.timeout, @@ -5382,6 +5399,33 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const message = uuidv4(); expect(await client.echo(message)).toEqual(message); + expect( + client instanceof GlideClient + ? await client.echo(message, Decoder.String) + : await client.echo(message, { + decoder: Decoder.String, + }), + ).toEqual(message); + expect( + client instanceof GlideClient + ? await client.echo(message, Decoder.Bytes) + : await client.echo(message, { + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from(message)); + expect( + client instanceof GlideClient + ? await client.echo( + Buffer.from(message), + Decoder.String, + ) + : await client.echo(Buffer.from(message), { + decoder: Decoder.String, + }), + ).toEqual(message); + expect(await client.echo(Buffer.from(message))).toEqual( + message, + ); }, protocol); }, config.timeout, From 82cbd33419310fca121c9948c753bd3c9d882dca Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 23 Aug 2024 13:56:39 -0700 Subject: [PATCH 217/236] Node: Add binary variant to server management commands. (#2179) * Add binary variant to server management commands. Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 10 +- node/src/Commands.ts | 35 +---- node/src/GlideClient.ts | 103 ++++++++----- node/src/GlideClusterClient.ts | 203 ++++++++++++++++---------- node/src/Transaction.ts | 54 ++++--- node/tests/GlideClusterClient.test.ts | 54 ++++--- node/tests/SharedTests.ts | 27 ++-- 9 files changed, 294 insertions(+), 195 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7296b9e38..b961a6c0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to server management commands ([#2179](https://github.com/valkey-io/valkey-glide/pull/2179)) * Node: Added/updated binary variant to connection management commands and WATCH/UNWATCH ([#2160](https://github.com/valkey-io/valkey-glide/pull/2160)) * Java: Fix docs for stream commands ([#2086](https://github.com/valkey-io/valkey-glide/pull/2086)) * Node: Added binary variant to bitmap commands ([#2178](https://github.com/valkey-io/valkey-glide/pull/2178)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 68efeadfda..65840cf7a8 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -118,6 +118,7 @@ function initialize() { SlotKeyTypes, TimeUnit, RouteByAddress, + RouteOption, Routes, RestoreOptions, SingleNodeRoute, @@ -227,6 +228,7 @@ function initialize() { TimeUnit, ReturnTypeXinfoStream, RouteByAddress, + RouteOption, Routes, RestoreOptions, SingleNodeRoute, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 16d23e62e9..123221718c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -24,7 +24,6 @@ import { BitwiseOperation, Boundary, CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars - DecoderOption, ExpireOptions, GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -289,6 +288,15 @@ export enum Decoder { String, } +/** An extension to command option types with {@link Decoder}. */ +export type DecoderOption = { + /** + * {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. + */ + decoder?: Decoder; +}; + /** * Our purpose in creating PointerResponse type is to mark when response is of number/long pointer response type. * Consequently, when the response is returned, we can check whether it is instanceof the PointerResponse type and pass it to the Rust core function with the proper parameters. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0b7aeb3c73..d70338a349 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -6,11 +6,11 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { BaseClient, Decoder } from "src/BaseClient"; +import { BaseClient } from "src/BaseClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { GlideClient } from "src/GlideClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { GlideClusterClient, Routes } from "src/GlideClusterClient"; +import { GlideClusterClient } from "src/GlideClusterClient"; import { GlideString } from "./BaseClient"; import { command_request } from "./ProtobufMessage"; @@ -89,24 +89,6 @@ function createCommand( return singleCommand; } -/** An extension to command option types with {@link Decoder}. */ -export type DecoderOption = { - /** - * {@link Decoder} type which defines how to handle the response. - * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. - */ - decoder?: Decoder; -}; - -/** An extension to command option types with {@link Routes}. */ -export type RouteOption = { - /** - * Specifies the routing configuration for the command. - * The client will route the command to the nodes defined by `route`. - */ - route?: Routes; -}; - /** * @internal */ @@ -408,7 +390,7 @@ export function createConfigGet(parameters: string[]): command_request.Command { * @internal */ export function createConfigSet( - parameters: Record, + parameters: Record, ): command_request.Command { return createCommand( RequestType.ConfigSet, @@ -2902,6 +2884,7 @@ export function createObjectRefcount( return createCommand(RequestType.ObjectRefCount, [key]); } +/** Additional parameters for `LOLWUT` command. */ export type LolwutOptions = { /** * An optional argument that can be used to specify the version of computer art to generate. @@ -2909,16 +2892,10 @@ export type LolwutOptions = { 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. + * - 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[]; - /** - * An optional argument specifies the type of decoding. - * Use Decoder.String to get the response as a String. - * Use Decoder.Bytes to get the response in a buffer. - */ - decoder?: Decoder; }; /** diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index ac2ed3f1ec..ef64b570a1 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -7,13 +7,13 @@ import { BaseClient, BaseClientConfiguration, Decoder, + DecoderOption, GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { - DecoderOption, FlushMode, FunctionListOptions, FunctionListResponse, @@ -260,15 +260,19 @@ export class GlideClient extends BaseClient { }); } - /** Get information and statistics about the Redis server. + /** + * Gets information and statistics about the server. + * * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * - * @param options - A list of InfoSection values specifying which sections of information to retrieve. - * When no parameter is provided, the default option is assumed. - * @returns a string containing the information for the sections requested. + * @param sections - (Optional) A list of {@link InfoOptions} values specifying which sections of information to retrieve. + * When no parameter is provided, {@link InfoOptions.Default|Default} is assumed. + * @returns A string containing the information for the sections requested. */ - public async info(options?: InfoOptions[]): Promise { - return this.createWritePromise(createInfo(options)); + public async info(sections?: InfoOptions[]): Promise { + return this.createWritePromise(createInfo(sections), { + decoder: Decoder.String, + }); } /** @@ -314,7 +318,9 @@ export class GlideClient extends BaseClient { }); } - /** Rewrite the configuration file with the current configuration. + /** + * Rewrites the configuration file with the current configuration. + * * @see {@link https://valkey.io/commands/config-rewrite/|valkey.io} for details. * * @returns "OK" when the configuration was rewritten properly. Otherwise, an error is thrown. @@ -327,10 +333,13 @@ export class GlideClient extends BaseClient { * ``` */ public async configRewrite(): Promise<"OK"> { - return this.createWritePromise(createConfigRewrite()); + return this.createWritePromise(createConfigRewrite(), { + decoder: Decoder.String, + }); } - /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + /** + * Resets the statistics reported by the server using the `INFO` and `LATENCY HISTOGRAM` commands. * * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * @@ -344,7 +353,9 @@ export class GlideClient extends BaseClient { * ``` */ public async configResetStat(): Promise<"OK"> { - return this.createWritePromise(createConfigResetStat()); + return this.createWritePromise(createConfigResetStat(), { + decoder: Decoder.String, + }); } /** @@ -364,11 +375,14 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createClientId()); } - /** Reads the configuration parameters of a running Redis server. + /** + * Reads the configuration parameters of the running server. * * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * * @returns A map of values corresponding to the configuration parameters. * @@ -381,16 +395,19 @@ export class GlideClient extends BaseClient { */ public async configGet( parameters: string[], - ): Promise> { - return this.createWritePromise(createConfigGet(parameters)); + decoder?: Decoder, + ): Promise> { + return this.createWritePromise(createConfigGet(parameters), { + decoder: decoder, + }); } /** - * Set configuration parameters to the specified values. + * Sets configuration parameters to the specified values. * * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. - * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. - * @returns "OK" when the configuration was set properly. Otherwise an error is thrown. + * @param parameters - A map consisting of configuration parameters and their respective values to set. + * @returns `"OK"` when the configuration was set properly. Otherwise an error is thrown. * * @example * ```typescript @@ -399,8 +416,12 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async configSet(parameters: Record): Promise<"OK"> { - return this.createWritePromise(createConfigSet(parameters)); + public async configSet( + parameters: Record, + ): Promise<"OK"> { + return this.createWritePromise(createConfigSet(parameters), { + decoder: Decoder.String, + }); } /** @@ -429,22 +450,24 @@ export class GlideClient extends BaseClient { }); } - /** Returns the server time + /** + * Returns the server time. + * * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * - * @returns - The current server time as a two items `array`: - * A Unix timestamp and the amount of microseconds already elapsed in the current second. - * The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. + * @returns The current server time as an `array` with two items: + * - A Unix timestamp, + * - The amount of microseconds already elapsed in the current second. * * @example * ```typescript - * // Example usage of time command - * const result = await client.time(); - * console.log(result); // Output: ['1710925775', '913580'] + * console.log(await client.time()); // Output: ['1710925775', '913580'] * ``` */ public async time(): Promise<[string, string]> { - return this.createWritePromise(createTime()); + return this.createWritePromise(createTime(), { + decoder: Decoder.String, + }); } /** @@ -513,7 +536,7 @@ export class GlideClient extends BaseClient { * * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for more details. * - * @param options - The LOLWUT options + * @param options - (Optional) The LOLWUT options - see {@link LolwutOptions}. * @returns A piece of generative computer art along with the current server version. * * @example @@ -523,7 +546,9 @@ export class GlideClient extends BaseClient { * ``` */ public async lolwut(options?: LolwutOptions): Promise { - return this.createWritePromise(createLolwut(options)); + return this.createWritePromise(createLolwut(options), { + decoder: Decoder.String, + }); } /** @@ -741,8 +766,8 @@ export class GlideClient extends BaseClient { * * @see {@link https://valkey.io/commands/flushall/|valkey.io} for more details. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @returns `OK`. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns `"OK"`. * * @example * ```typescript @@ -750,8 +775,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async flushall(mode?: FlushMode): Promise { - return this.createWritePromise(createFlushAll(mode)); + public async flushall(mode?: FlushMode): Promise<"OK"> { + return this.createWritePromise(createFlushAll(mode), { + decoder: Decoder.String, + }); } /** @@ -759,8 +786,8 @@ export class GlideClient extends BaseClient { * * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for more details. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @returns `OK`. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns `"OK"`. * * @example * ```typescript @@ -768,8 +795,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async flushdb(mode?: FlushMode): Promise { - return this.createWritePromise(createFlushDB(mode)); + public async flushdb(mode?: FlushMode): Promise<"OK"> { + return this.createWritePromise(createFlushDB(mode), { + decoder: Decoder.String, + }); } /** diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index ed3492cd3d..193d49b7d5 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -7,13 +7,13 @@ import { BaseClient, BaseClientConfiguration, Decoder, + DecoderOption, GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { - DecoderOption, FlushMode, FunctionListOptions, FunctionListResponse, @@ -21,7 +21,6 @@ import { FunctionStatsSingleResponse, InfoOptions, LolwutOptions, - RouteOption, SortClusterOptions, createClientGetName, createClientId, @@ -62,6 +61,15 @@ import { RequestError } from "./Errors"; import { command_request, connection_request } from "./ProtobufMessage"; import { ClusterTransaction } from "./Transaction"; +/** An extension to command option types with {@link Routes}. */ +export type RouteOption = { + /** + * Specifies the routing configuration for the command. + * The client will route the command to the nodes defined by `route`. + */ + route?: Routes; +}; + /** * Represents a manually configured interval for periodic checks. */ @@ -449,23 +457,27 @@ export class GlideClusterClient extends BaseClient { }); } - /** Get information and statistics about the Redis server. + /** + * Gets information and statistics about the server. + * + * The command will be routed to all primary nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * - * @param options - A list of InfoSection values specifying which sections of information to retrieve. - * When no parameter is provided, the default option is assumed. - * @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 a string containing the information for the sections requested. When specifying a route other than a single node, - * it returns a dictionary where each address is the key and its corresponding node response is the value. + * @param options - (Optional) Additional parameters: + * - (Optional) `sections`: a list of {@link InfoOptions} values specifying which sections of information to retrieve. + * When no parameter is provided, {@link InfoOptions.Default|Default} is assumed. + * - (Optional) `route`: see {@link RouteOption}. + * @returns A string containing the information for the sections requested. + * When specifying a route other than a single node, + * it returns a dictionary where each address is the key and its corresponding node response is the value. */ public async info( - options?: InfoOptions[], - route?: Routes, + options?: { sections?: InfoOptions[] } & RouteOption, ): Promise> { return this.createWritePromise>( - createInfo(options), - { route: toProtobufRoute(route) }, + createInfo(options?.sections), + { route: toProtobufRoute(options?.route), decoder: Decoder.String }, ); } @@ -508,12 +520,16 @@ export class GlideClusterClient extends BaseClient { ); } - /** Rewrite the configuration file with the current configuration. + /** + * Rewrites the configuration file with the current configuration. + * + * The command will be routed to a all nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/config-rewrite/|valkey.io} for details. * - * @param route - The command will be routed to all nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. - * @returns "OK" when the configuration was rewritten properly. Otherwise, an error is thrown. + * @param route - (Optional) Specifies the routing configuration for the command. + * The client will route the command to the nodes defined by `route`. + * @returns `"OK"` when the configuration was rewritten properly. Otherwise, an error is thrown. * * @example * ```typescript @@ -525,15 +541,20 @@ export class GlideClusterClient extends BaseClient { public async configRewrite(route?: Routes): Promise<"OK"> { return this.createWritePromise(createConfigRewrite(), { route: toProtobufRoute(route), + decoder: Decoder.String, }); } - /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + /** + * Resets the statistics reported by the server using the `INFO` and `LATENCY HISTOGRAM` commands. + * + * The command will be routed to all nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * - * @param route - The command will be routed to all nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. - * @returns always "OK". + * @param route - (Optional) Specifies the routing configuration for the command. + * The client will route the command to the nodes defined by `route`. + * @returns always `"OK"`. * * @example * ```typescript @@ -545,6 +566,7 @@ export class GlideClusterClient extends BaseClient { public async configResetStat(route?: Routes): Promise<"OK"> { return this.createWritePromise(createConfigResetStat(), { route: toProtobufRoute(route), + decoder: Decoder.String, }); } @@ -574,16 +596,18 @@ export class GlideClusterClient extends BaseClient { ); } - /** Reads the configuration parameters of a running Redis server. + /** + * Reads the configuration parameters of the running server. + * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. - * @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`. - * If `route` is not provided, the command will be sent to a random node. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * * @returns A map of values corresponding to the configuration parameters. When specifying a route other than a single node, - * it returns a dictionary where each address is the key and its corresponding node response is the value. + * it returns a dictionary where each address is the key and its corresponding node response is the value. * * @example * ```typescript @@ -601,21 +625,23 @@ export class GlideClusterClient extends BaseClient { */ public async configGet( parameters: string[], - route?: Routes, - ): Promise>> { - return this.createWritePromise>>( - createConfigGet(parameters), - { route: toProtobufRoute(route) }, - ); + options?: RouteOption & DecoderOption, + ): Promise>> { + return this.createWritePromise(createConfigGet(parameters), { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, + }); } - /** Set configuration parameters to the specified values. + /** + * Sets configuration parameters to the specified values. + * + * The command will be routed to all nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * - * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. - * @param route - The command will be routed to all nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. - * If `route` is not provided, the command will be sent to the all nodes. + * @param parameters - A map consisting of configuration parameters and their respective values to set. + * @param options - (Optional) See {@link RouteOption}. * @returns "OK" when the configuration was set properly. Otherwise an error is thrown. * * @example @@ -626,11 +652,12 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async configSet( - parameters: Record, - route?: Routes, + parameters: Record, + options?: RouteOption, ): Promise<"OK"> { return this.createWritePromise(createConfigSet(parameters), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } @@ -669,15 +696,19 @@ export class GlideClusterClient extends BaseClient { }); } - /** Returns the server time. + /** + * Returns the server time. + * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * - * @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`. + * @param options - (Optional) See {@link RouteOption}. + * + * @returns The current server time as an `array` with two items: + * - A Unix timestamp, + * - The amount of microseconds already elapsed in the current second. * - * @returns - The current server time as a two items `array`: - * A Unix timestamp and the amount of microseconds already elapsed in the current second. - * The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. * When specifying a route other than a single node, it returns a dictionary where each address is the key and * its corresponding node response is the value. * @@ -696,10 +727,11 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async time( - route?: Routes, + options?: RouteOption, ): Promise> { return this.createWritePromise(createTime(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } @@ -736,11 +768,11 @@ export class GlideClusterClient extends BaseClient { /** * Displays a piece of generative computer art and the server version. * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for 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`. + * @param options - (Optional) The LOLWUT options - see {@link LolwutOptions} and {@link RouteOption}. * @returns A piece of generative computer art along with the current server version. * * @example @@ -750,12 +782,11 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async lolwut( - options?: LolwutOptions, - route?: Routes, + options?: LolwutOptions & RouteOption, ): Promise> { return this.createWritePromise(createLolwut(options), { - decoder: options?.decoder, - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } @@ -1074,11 +1105,13 @@ export class GlideClusterClient extends BaseClient { /** * Deletes all the keys of all the existing databases. This command never fails. * + * The command will be routed to all primary nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/flushall/|valkey.io} for 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 primary nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. + * @param options - (Optional) Additional parameters: + * - (Optional) `mode`: the flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * - (Optional) `route`: see {@link RouteOption}. * @returns `OK`. * * @example @@ -1087,20 +1120,27 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async flushall(mode?: FlushMode, route?: Routes): Promise { - return this.createWritePromise(createFlushAll(mode), { - route: toProtobufRoute(route), + public async flushall( + options?: { + mode?: FlushMode; + } & RouteOption, + ): Promise<"OK"> { + return this.createWritePromise(createFlushAll(options?.mode), { + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } /** * Deletes all the keys of the currently selected database. This command never fails. * + * The command will be routed to all primary nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for 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 primary nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. + * @param options - (Optional) Additional parameters: + * - (Optional) `mode`: the flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * - (Optional) `route`: see {@link RouteOption}. * @returns `OK`. * * @example @@ -1109,19 +1149,25 @@ export class GlideClusterClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async flushdb(mode?: FlushMode, route?: Routes): Promise { - return this.createWritePromise(createFlushDB(mode), { - route: toProtobufRoute(route), + public async flushdb( + options?: { + mode?: FlushMode; + } & RouteOption, + ): Promise<"OK"> { + return this.createWritePromise(createFlushDB(options?.mode), { + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } /** * Returns the number of keys in the database. * + * The command will be routed to all nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/dbsize/|valkey.io} for details. - - * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. + * + * @param options - (Optional) See {@link RouteOption}. * @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. * @@ -1131,9 +1177,11 @@ export class GlideClusterClient extends BaseClient { * console.log("Number of keys across all primary nodes: ", numKeys); * ``` */ - public async dbsize(route?: Routes): Promise> { + public async dbsize( + options?: RouteOption, + ): Promise> { return this.createWritePromise(createDBSize(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), }); } @@ -1321,10 +1369,11 @@ export class GlideClusterClient extends BaseClient { * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/lastsave/|valkey.io} for details. * - * @param route - (Optional) 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`. + * @param options - (Optional) See {@link RouteOption}. * @returns `UNIX TIME` of the last DB save executed with success. * * @example @@ -1333,9 +1382,11 @@ export class GlideClusterClient extends BaseClient { * console.log("Last DB save was done at " + timestamp); * ``` */ - public async lastsave(route?: Routes): Promise> { + public async lastsave( + options?: RouteOption, + ): Promise> { return this.createWritePromise(createLastSave(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), }); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index f4982236e1..0b9765b281 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -405,16 +405,18 @@ export class BaseTransaction> { return this.addAndReturn(createPing(message)); } - /** Get information and statistics about the Redis server. + /** + * Gets information and statistics about the server. + * * @see {@link https://valkey.io/commands/info/|valkey.io} for details. * - * @param options - A list of InfoSection values specifying which sections of information to retrieve. - * When no parameter is provided, the default option is assumed. + * @param sections - (Optional) A list of {@link InfoOptions} values specifying which sections of information to retrieve. + * When no parameter is provided, {@link InfoOptions.Default|Default} is assumed. * - * Command Response - a string containing the information for the sections requested. + * Command Response - A string containing the information for the sections requested. */ - public info(options?: InfoOptions[]): T { - return this.addAndReturn(createInfo(options)); + public info(sections?: InfoOptions[]): T { + return this.addAndReturn(createInfo(sections)); } /** @@ -478,7 +480,9 @@ export class BaseTransaction> { return this.addAndReturn(createClientGetName()); } - /** Rewrite the configuration file with the current configuration. + /** + * Rewrites the configuration file with the current configuration. + * * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * Command Response - "OK" when the configuration was rewritten properly. Otherwise, the transaction fails with an error. @@ -487,7 +491,9 @@ export class BaseTransaction> { return this.addAndReturn(createConfigRewrite()); } - /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + /** + * Resets the statistics reported by Redis using the `INFO` and `LATENCY HISTOGRAM` commands. + * * @see {@link https://valkey.io/commands/config-resetstat/|valkey.io} for details. * * Command Response - always "OK". @@ -755,7 +761,9 @@ export class BaseTransaction> { return this.addAndReturn(createBitField(key, subcommands, true)); } - /** Reads the configuration parameters of a running Redis server. + /** + * Reads the configuration parameters of the running server. + * * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * * @param parameters - A list of configuration parameter names to retrieve values for. @@ -767,14 +775,16 @@ export class BaseTransaction> { return this.addAndReturn(createConfigGet(parameters)); } - /** Set configuration parameters to the specified values. + /** + * Sets configuration parameters to the specified values. + * * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * - * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. + * @param parameters - A map consisting of configuration parameters and their respective values to set. * * Command Response - "OK" when the configuration was set properly. Otherwise, the transaction fails with an error. */ - public configSet(parameters: Record): T { + public configSet(parameters: Record): T { return this.addAndReturn(createConfigSet(parameters)); } @@ -2541,12 +2551,14 @@ export class BaseTransaction> { return this.addAndReturn(createXInfoGroups(key)); } - /** Returns the server time. + /** + * Returns the server time. + * * @see {@link https://valkey.io/commands/time/|valkey.io} for details. * - * Command Response - The current server time as a two items `array`: - * A Unix timestamp and the amount of microseconds already elapsed in the current second. - * The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. + * Command Response - The current server time as an `array` with two items: + * - A Unix timestamp, + * - The amount of microseconds already elapsed in the current second. */ public time(): T { return this.addAndReturn(createTime()); @@ -3139,7 +3151,7 @@ export class BaseTransaction> { * * @see {@link https://valkey.io/commands/lolwut/|valkey.io} for details. * - * @param options - The LOLWUT options. + * @param options - (Optional) The LOLWUT options - see {@link LolwutOptions}. * * Command Response - A piece of generative computer art along with the current server version. */ @@ -3304,9 +3316,9 @@ export class BaseTransaction> { * * @see {@link https://valkey.io/commands/flushall/|valkey.io} for details. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * - * Command Response - `OK`. + * Command Response - `"OK"`. */ public flushall(mode?: FlushMode): T { return this.addAndReturn(createFlushAll(mode)); @@ -3317,9 +3329,9 @@ export class BaseTransaction> { * * @see {@link https://valkey.io/commands/flushdb/|valkey.io} for details. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. * - * Command Response - `OK`. + * Command Response - `"OK"`. */ public flushdb(mode?: FlushMode): T { return this.addAndReturn(createFlushDB(mode)); diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index c1f7f4e389..4f7a6203bd 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -111,12 +111,12 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); const info_server = getFirstResult( - await client.info([InfoOptions.Server]), + await client.info({ sections: [InfoOptions.Server] }), ); expect(info_server).toEqual(expect.stringContaining("# Server")); const infoReplicationValues = Object.values( - await client.info([InfoOptions.Replication]), + await client.info({ sections: [InfoOptions.Replication] }), ); const replicationInfo = intoArray(infoReplicationValues); @@ -135,10 +135,10 @@ describe("GlideClusterClient", () => { client = await GlideClusterClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), ); - const result = await client.info( - [InfoOptions.Server], - "randomNode", - ); + const result = await client.info({ + sections: [InfoOptions.Server], + route: "randomNode", + }); expect(result).toEqual(expect.stringContaining("# Server")); expect(result).toEqual(expect.not.stringContaining("# Errorstats")); }, @@ -216,9 +216,11 @@ describe("GlideClusterClient", () => { getClientConfigurationOption(cluster.getAddresses(), protocol), ); await expect( - client.info(undefined, { - type: "routeByAddress", - host: "foo", + client.info({ + route: { + type: "routeByAddress", + host: "foo", + }, }), ).rejects.toThrowError(RequestError); }, @@ -571,29 +573,31 @@ describe("GlideClusterClient", () => { ); // test with multi-node route - const result1 = await client.lolwut({}, "allNodes"); + const result1 = await client.lolwut({ route: "allNodes" }); expect(intoString(result1)).toEqual( expect.stringContaining("Redis ver. "), ); - const result2 = await client.lolwut( - { version: 2, parameters: [10, 20] }, - "allNodes", - ); + const result2 = await client.lolwut({ + version: 2, + parameters: [10, 20], + route: "allNodes", + }); expect(intoString(result2)).toEqual( expect.stringContaining("Redis ver. "), ); // test with single-node route - const result3 = await client.lolwut({}, "randomNode"); + const result3 = await client.lolwut({ route: "randomNode" }); expect(intoString(result3)).toEqual( expect.stringContaining("Redis ver. "), ); - const result4 = await client.lolwut( - { version: 2, parameters: [10, 20] }, - "randomNode", - ); + const result4 = await client.lolwut({ + version: 2, + parameters: [10, 20], + route: "randomNode", + }); expect(intoString(result4)).toEqual( expect.stringContaining("Redis ver. "), ); @@ -698,12 +702,16 @@ describe("GlideClusterClient", () => { expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); + expect(await client.flushdb({ mode: FlushMode.ASYNC })).toEqual( + "OK", + ); expect(await client.dbsize()).toEqual(0); expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.flushdb({ mode: FlushMode.SYNC })).toEqual( + "OK", + ); expect(await client.dbsize()).toEqual(0); client.close(); @@ -1625,7 +1633,9 @@ describe("GlideClusterClient", () => { const key = uuidv4(); // setup: delete all keys - expect(await client.flushall(FlushMode.SYNC)).toEqual("OK"); + expect(await client.flushall({ mode: FlushMode.SYNC })).toEqual( + "OK", + ); // no keys exist so randomKey returns null expect(await client.randomKey()).toBeNull(); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 97ae5107ba..0b247416a7 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -311,7 +311,9 @@ export function runBaseTests(config: { client instanceof GlideClient ? await client.info([InfoOptions.Commandstats]) : Object.values( - await client.info([InfoOptions.Commandstats]), + await client.info({ + sections: [InfoOptions.Commandstats], + }), ).join(); expect(oldResult).toContain("cmdstat_set"); expect(await client.configResetStat()).toEqual("OK"); @@ -320,7 +322,9 @@ export function runBaseTests(config: { client instanceof GlideClient ? await client.info([InfoOptions.Commandstats]) : Object.values( - await client.info([InfoOptions.Commandstats]), + await client.info({ + sections: [InfoOptions.Commandstats], + }), ).join(); expect(result).not.toContain("cmdstat_set"); }, protocol); @@ -339,9 +343,9 @@ export function runBaseTests(config: { expect(await client.lastsave()).toBeGreaterThan(yesterday); if (client instanceof GlideClusterClient) { - Object.values(await client.lastsave("allNodes")).forEach( - (v) => expect(v).toBeGreaterThan(yesterday), - ); + Object.values( + await client.lastsave({ route: "allNodes" }), + ).forEach((v) => expect(v).toBeGreaterThan(yesterday)); } const response = @@ -7785,11 +7789,14 @@ export function runBaseTests(config: { type: "primarySlotKey", key: key, }; - expect(await client.flushall(undefined, primaryRoute)).toBe( + expect(await client.flushall({ route: primaryRoute })).toBe( "OK", ); expect( - await client.flushall(FlushMode.ASYNC, primaryRoute), + await client.flushall({ + mode: FlushMode.ASYNC, + route: primaryRoute, + }), ).toBe("OK"); //Test FLUSHALL on replica (should fail) @@ -7799,7 +7806,7 @@ export function runBaseTests(config: { key: key2, }; await expect( - client.flushall(undefined, replicaRoute), + client.flushall({ route: replicaRoute }), ).rejects.toThrowError(); } }, protocol); @@ -7935,7 +7942,9 @@ export function runBaseTests(config: { type: "primarySlotKey", key: key, }; - expect(await client.dbsize(primaryRoute)).toBe(1); + expect(await client.dbsize({ route: primaryRoute })).toBe( + 1, + ); } }, protocol); }, From 1b14aeb663059508cc5cd8406c814ce1e237b997 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Sun, 25 Aug 2024 00:23:58 +0000 Subject: [PATCH 218/236] Updated attribution files Signed-off-by: ort-bot --- glide-core/THIRD_PARTY_LICENSES_RUST | 10 +++++----- java/THIRD_PARTY_LICENSES_JAVA | 10 +++++----- node/THIRD_PARTY_LICENSES_NODE | 12 ++++++------ python/THIRD_PARTY_LICENSES_PYTHON | 10 +++++----- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index d524c5e3f5..6902047fb8 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -7076,7 +7076,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.32 +Package: flate2:1.0.33 The following copyrights and licenses were found in the source code of this package: @@ -18828,7 +18828,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: quote:1.0.36 +Package: quote:1.0.37 The following copyrights and licenses were found in the source code of this package: @@ -22205,7 +22205,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.208 +Package: serde:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -22434,7 +22434,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.208 +Package: serde_derive:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -23537,7 +23537,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.75 +Package: syn:2.0.76 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 c88d1c23d7..fc65035b85 100644 --- a/java/THIRD_PARTY_LICENSES_JAVA +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -7305,7 +7305,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.32 +Package: flate2:1.0.33 The following copyrights and licenses were found in the source code of this package: @@ -19723,7 +19723,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: quote:1.0.36 +Package: quote:1.0.37 The following copyrights and licenses were found in the source code of this package: @@ -23100,7 +23100,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.208 +Package: serde:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -23329,7 +23329,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.208 +Package: serde_derive:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -24432,7 +24432,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.75 +Package: syn:2.0.76 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 2005cc23e4..8c667ec3ab 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -7382,7 +7382,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.32 +Package: flate2:1.0.33 The following copyrights and licenses were found in the source code of this package: @@ -19485,7 +19485,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: quote:1.0.36 +Package: quote:1.0.37 The following copyrights and licenses were found in the source code of this package: @@ -23778,7 +23778,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.208 +Package: serde:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -24007,7 +24007,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.208 +Package: serde_derive:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -25110,7 +25110,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.75 +Package: syn:2.0.76 The following copyrights and licenses were found in the source code of this package: @@ -37510,7 +37510,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobufjs:7.3.3 +Package: protobufjs:7.4.0 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 7e07561a8c..1a87cff41e 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -7076,7 +7076,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: flate2:1.0.32 +Package: flate2:1.0.33 The following copyrights and licenses were found in the source code of this package: @@ -20893,7 +20893,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: quote:1.0.36 +Package: quote:1.0.37 The following copyrights and licenses were found in the source code of this package: @@ -24270,7 +24270,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.208 +Package: serde:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -24499,7 +24499,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.208 +Package: serde_derive:1.0.209 The following copyrights and licenses were found in the source code of this package: @@ -25602,7 +25602,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.75 +Package: syn:2.0.76 The following copyrights and licenses were found in the source code of this package: From ed339173b63c482bb7b890c36fa74d6af8bd121a Mon Sep 17 00:00:00 2001 From: adarovadya Date: Sun, 25 Aug 2024 14:53:54 +0300 Subject: [PATCH 219/236] add decoder to scripts (#2157) Signed-off-by: Adar Ovadia Co-authored-by: Adar Ovadia --- node/src/BaseClient.ts | 24 ++++++++------ node/tests/SharedTests.ts | 66 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 123221718c..f3f0e5c57a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -410,11 +410,15 @@ export type ScriptOptions = { /** * The keys that are used in the script. */ - keys?: (string | Uint8Array)[]; + keys?: GlideString[]; /** * The arguments for the script. */ - args?: (string | Uint8Array)[]; + args?: GlideString[]; + /** + * {@link Decoder} type which defines how to handle the responses. If not set, the default decoder from the client config will be used. + */ + decoder?: Decoder; }; function getRequestErrorClass( @@ -1088,8 +1092,8 @@ export class BaseClient { * ``` */ public async set( - key: string | Uint8Array, - value: string | Uint8Array, + key: GlideString, + value: GlideString, options?: SetOptions, ): Promise<"OK" | string | null> { return this.createWritePromise(createSet(key, value, options)); @@ -3282,24 +3286,26 @@ export class BaseClient { hash: script.getHash(), keys: option?.keys?.map((item) => { if (typeof item === "string") { - // Convert the string to a Uint8Array + // Convert the string to a Buffer return Buffer.from(item); } else { - // If it's already a Uint8Array, just return it + // If it's already a Buffer, just return it return item; } }), args: option?.args?.map((item) => { if (typeof item === "string") { - // Convert the string to a Uint8Array + // Convert the string to a Buffer return Buffer.from(item); } else { - // If it's already a Uint8Array, just return it + // If it's already a Buffer, just return it return item; } }), }); - return this.createWritePromise(scriptInvocation); + return this.createWritePromise(scriptInvocation, { + decoder: option?.decoder, + }); } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0b247416a7..645a37dd6d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3619,14 +3619,18 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `script test_%p`, + `script test_decoder_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = Buffer.from(uuidv4()); const key2 = Buffer.from(uuidv4()); let script = new Script(Buffer.from("return 'Hello'")); - expect(await client.invokeScript(script)).toEqual("Hello"); + expect( + await client.invokeScript(script, { + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from("Hello")); script = new Script( Buffer.from("return redis.call('SET', KEYS[1], ARGV[1])"), @@ -3635,6 +3639,7 @@ export function runBaseTests(config: { await client.invokeScript(script, { keys: [key1], args: [Buffer.from("value1")], + decoder: Decoder.Bytes, }), ).toEqual("OK"); @@ -3656,6 +3661,20 @@ export function runBaseTests(config: { expect( await client.invokeScript(script, { keys: [key2] }), ).toEqual("value2"); + // Get bytes rsponse + expect( + await client.invokeScript(script, { + keys: [key1], + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from("value1")); + + expect( + await client.invokeScript(script, { + keys: [key2], + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from("value2")); }, protocol); }, config.timeout, @@ -3663,6 +3682,49 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `script test_binary_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = Buffer.from(uuidv4()); + const key2 = Buffer.from(uuidv4()); + + let script = new Script(Buffer.from("return 'Hello'")); + expect(await client.invokeScript(script)).toEqual("Hello"); + + script = new Script( + Buffer.from("return redis.call('SET', KEYS[1], ARGV[1])"), + ); + expect( + await client.invokeScript(script, { + keys: [key1], + args: [Buffer.from("value1")], + }), + ).toEqual("OK"); + + /// Reuse the same script with different parameters. + expect( + await client.invokeScript(script, { + keys: [key2], + args: [Buffer.from("value2")], + }), + ).toEqual("OK"); + + script = new Script( + Buffer.from("return redis.call('GET', KEYS[1])"), + ); + expect( + await client.invokeScript(script, { keys: [key1] }), + ).toEqual("value1"); + + expect( + await client.invokeScript(script, { keys: [key2] }), + ).toEqual("value2"); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `script test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); From 2276fd2e159067a517dd8b59bdd6d209d4ad4107 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Mon, 26 Aug 2024 00:22:55 +0000 Subject: [PATCH 220/236] Updated attribution files Signed-off-by: ort-bot --- node/THIRD_PARTY_LICENSES_NODE | 2 +- python/THIRD_PARTY_LICENSES_PYTHON | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 8c667ec3ab..fd31743b0c 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -36014,7 +36014,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: eslint-module-utils:2.8.1 +Package: eslint-module-utils:2.8.2 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 1a87cff41e..65bd19e880 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -38631,7 +38631,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pyparsing:3.1.2 +Package: pyparsing:3.1.4 The following copyrights and licenses were found in the source code of this package: From e1811c40a8f76726c6e628c5dd5f4a62529cf0e5 Mon Sep 17 00:00:00 2001 From: adarovadya Date: Mon, 26 Aug 2024 13:22:22 +0300 Subject: [PATCH 221/236] Node: warp with try-catch the rust call and tests (#2182) * warp with try-catch the rust call and tests * add test --------- Signed-off-by: Adar Ovadia Co-authored-by: Adar Ovadia --- node/src/BaseClient.ts | 39 ++++++++++++++++----------- node/tests/GlideClient.test.ts | 33 +++++++++++++++++++++++ node/tests/GlideClusterClient.test.ts | 7 +++-- node/tests/TestUtilities.ts | 37 +++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 17 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index f3f0e5c57a..1617d7b707 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -680,23 +680,32 @@ export class BaseClient { const callbackIndex = this.getCallbackIndex(); this.promiseCallbackFunctions[callbackIndex] = [ (resolveAns: T) => { - if (resolveAns instanceof PointerResponse) { - if (typeof resolveAns === "number") { - resolveAns = valueFromSplitPointer( - 0, - resolveAns, - stringDecoder, - ) as T; - } else { - resolveAns = valueFromSplitPointer( - resolveAns.high!, - resolveAns.low!, - stringDecoder, - ) as T; + try { + if (resolveAns instanceof PointerResponse) { + if (typeof resolveAns === "number") { + resolveAns = valueFromSplitPointer( + 0, + resolveAns, + stringDecoder, + ) as T; + } else { + resolveAns = valueFromSplitPointer( + resolveAns.high!, + resolveAns.low!, + stringDecoder, + ) as T; + } } - } - resolve(resolveAns); + resolve(resolveAns); + } catch (err) { + Logger.log( + "error", + "Decoder", + `Decoding error: '${err}'`, + ); + reject(err); + } }, reject, ]; diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 99f379e61c..f589f4d63f 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -32,6 +32,7 @@ import { checkFunctionStatsResponse, convertStringArrayToBuffer, createLuaLibWithLongRunningFunction, + DumpAndRestureTest, encodableTransactionTest, encodedTransactionTest, flushAndCloseClient, @@ -242,6 +243,38 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `dump and restore transactions_%p`, + async (protocol) => { + client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const bytesTransaction = new Transaction(); + const expectedBytesRes = await DumpAndRestureTest( + bytesTransaction, + Buffer.from("value"), + ); + bytesTransaction.select(0); + const result = await client.exec(bytesTransaction, Decoder.Bytes); + expectedBytesRes.push(["select(0)", "OK"]); + + validateTransactionResponse(result, expectedBytesRes); + + const stringTransaction = new Transaction(); + await DumpAndRestureTest(stringTransaction, "value"); + stringTransaction.select(0); + + // Since DUMP gets binary results, we cannot use the string decoder here, so we expected to get an error. + await expect( + client.exec(stringTransaction, Decoder.String), + ).rejects.toThrowError( + "invalid utf-8 sequence of 1 bytes from index 9", + ); + + client.close(); + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `can send transaction with default string decoder_%p`, async (protocol) => { diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 4f7a6203bd..9d4687f69e 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -239,11 +239,14 @@ describe("GlideClusterClient", () => { const valueEncoded = Buffer.from(value); expect(await client.set(key, value)).toEqual("OK"); // Since DUMP gets binary results, we cannot use the default decoder (string) here, so we expected to get an error. - // TODO: fix custom command with unmatch decoder to return an error: https://github.com/valkey-io/valkey-glide/issues/2119 - // expect(await client.customCommand(["DUMP", key])).toThrowError(); + await expect(client.customCommand(["DUMP", key])).rejects.toThrow( + "invalid utf-8 sequence of 1 bytes from index 9", + ); + const dumpResult = await client.customCommand(["DUMP", key], { decoder: Decoder.Bytes, }); + expect(await client.del([key])).toEqual(1); if (dumpResult instanceof Buffer) { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 5dbb9544d1..0b177e62f0 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -602,6 +602,43 @@ export async function encodedTransactionTest( return responseData; } +/** Populates a transaction with dump and restore commands + * + * @param baseTransaction - A transaction + * @param valueResponse - Represents the encoded response of "value" to compare + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ +export async function DumpAndRestureTest( + baseTransaction: Transaction, + valueResponse: GlideString, +): Promise<[string, ReturnType][]> { + const key = "dumpKey"; + const dumpResult = Buffer.from([ + 0, 5, 118, 97, 108, 117, 101, 11, 0, 232, 41, 124, 75, 60, 53, 114, 231, + ]); + const value = "value"; + // array of tuples - first element is test name/description, second - expected return value + const responseData: [string, ReturnType][] = []; + + baseTransaction.set(key, value); + responseData.push(["set(key, value)", "OK"]); + baseTransaction.customCommand(["DUMP", key]); + responseData.push(['customCommand(["DUMP", key])', dumpResult]); + baseTransaction.del([key]); + responseData.push(["del(key)", 1]); + baseTransaction.get(key); + responseData.push(["get(key)", null]); + baseTransaction.customCommand(["RESTORE", key, "0", dumpResult]); + responseData.push([ + 'customCommand(["RESTORE", key, "0", dumpResult])', + "OK", + ]); + baseTransaction.get(key); + responseData.push(["get(key)", valueResponse]); + + return responseData; +} + /** * Populates a transaction with commands to test. * @param baseTransaction - A transaction. From 610ff42232893e9d485957f3b58ceffbd3079a35 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Mon, 26 Aug 2024 10:18:21 -0700 Subject: [PATCH 222/236] Node: add binary variant to function commands (#2172) * Node: add binary variant to function commands --------- Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 30 ++-- node/src/Commands.ts | 40 +++--- node/src/GlideClient.ts | 55 +++++--- node/src/GlideClusterClient.ts | 115 ++++++++------- node/src/Transaction.ts | 26 ++-- node/tests/GlideClient.test.ts | 46 ++++-- node/tests/GlideClusterClient.test.ts | 195 +++++++++++++------------- node/tests/TestUtilities.ts | 2 +- 9 files changed, 295 insertions(+), 215 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b961a6c0a1..7361c001b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Node: Added binary variant to geo commands ([#2149](https://github.com/valkey-io/valkey-glide/pull/2149)) * Node: Added binary variant to HYPERLOGLOG commands ([#2176](https://github.com/valkey-io/valkey-glide/pull/2176)) * Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129), [#2173](https://github.com/valkey-io/valkey-glide/pull/2173)) +* Node: Added binary variant to FUNCTION commands ([#2172](https://github.com/valkey-io/valkey-glide/pull/2172)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1617d7b707..5e8989e4c7 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -5682,6 +5682,8 @@ export class BaseClient { * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The invoked function's return value. * * @example @@ -5691,11 +5693,14 @@ export class BaseClient { * ``` */ public async fcall( - func: string, - keys: string[], - args: string[], - ): Promise { - return this.createWritePromise(createFCall(func, keys, args)); + func: GlideString, + keys: GlideString[], + args: GlideString[], + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createFCall(func, keys, args), { + decoder, + }); } /** @@ -5709,6 +5714,8 @@ export class BaseClient { * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, * all names of keys that a function accesses must be explicitly provided as `keys`. * @param args - A list of `function` arguments and it should not represent names of keys. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns The invoked function's return value. * * @example @@ -5719,11 +5726,14 @@ export class BaseClient { * ``` */ public async fcallReadonly( - func: string, - keys: string[], - args: string[], - ): Promise { - return this.createWritePromise(createFCallReadOnly(func, keys, args)); + func: GlideString, + keys: GlideString[], + args: GlideString[], + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createFCallReadOnly(func, keys, args), { + decoder, + }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d70338a349..a3ed23a5bd 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2239,12 +2239,16 @@ export function createBLPop( * @internal */ export function createFCall( - func: string, - keys: string[], - args: string[], + func: GlideString, + keys: GlideString[], + args: GlideString[], ): command_request.Command { - let params: string[] = []; - params = params.concat(func, keys.length.toString(), keys, args); + const params: GlideString[] = [ + func, + keys.length.toString(), + ...keys, + ...args, + ]; return createCommand(RequestType.FCall, params); } @@ -2252,12 +2256,16 @@ export function createFCall( * @internal */ export function createFCallReadOnly( - func: string, - keys: string[], - args: string[], + func: GlideString, + keys: GlideString[], + args: GlideString[], ): command_request.Command { - let params: string[] = []; - params = params.concat(func, keys.length.toString(), keys, args); + const params: GlideString[] = [ + func, + keys.length.toString(), + ...keys, + ...args, + ]; return createCommand(RequestType.FCallReadOnly, params); } @@ -2265,7 +2273,7 @@ export function createFCallReadOnly( * @internal */ export function createFunctionDelete( - libraryCode: string, + libraryCode: GlideString, ): command_request.Command { return createCommand(RequestType.FunctionDelete, [libraryCode]); } @@ -2285,7 +2293,7 @@ export function createFunctionFlush(mode?: FlushMode): command_request.Command { * @internal */ export function createFunctionLoad( - libraryCode: string, + libraryCode: GlideString, replace?: boolean, ): command_request.Command { const args = replace ? ["REPLACE", libraryCode] : [libraryCode]; @@ -2295,7 +2303,7 @@ export function createFunctionLoad( /** Optional arguments for `FUNCTION LIST` command. */ export type FunctionListOptions = { /** A wildcard pattern for matching library names. */ - libNamePattern?: string; + libNamePattern?: GlideString; /** Specifies whether to request the library code from the server or not. */ withCode?: boolean; }; @@ -2303,7 +2311,7 @@ export type FunctionListOptions = { /** Type of the response of `FUNCTION LIST` command. */ export type FunctionListResponse = Record< string, - string | Record[] + GlideString | Record[] >[]; /** @@ -2312,7 +2320,7 @@ export type FunctionListResponse = Record< export function createFunctionList( options?: FunctionListOptions, ): command_request.Command { - const args: string[] = []; + const args: GlideString[] = []; if (options) { if (options.libNamePattern) { @@ -2335,7 +2343,7 @@ export function createFunctionList( export type FunctionStatsSingleResponse = Record< string, | null - | Record // Running function/script information + | Record // Running function/script information | Record> // Execution engines information >; diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index ef64b570a1..ccebc647a7 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -558,7 +558,7 @@ export class GlideClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The library name to delete. - * @returns A simple OK response. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -566,8 +566,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async functionDelete(libraryCode: string): Promise { - return this.createWritePromise(createFunctionDelete(libraryCode)); + public async functionDelete(libraryCode: GlideString): Promise<"OK"> { + return this.createWritePromise(createFunctionDelete(libraryCode), { + decoder: Decoder.String, + }); } /** @@ -577,8 +579,10 @@ export class GlideClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. - * @param replace - Whether the given library should overwrite a library with the same name if it + * @param options - (Optional) Additional parameters: + * - (Optional) `replace`: Whether the given library should overwrite a library with the same name if it * already exists. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns The library name that was loaded. * * @example @@ -589,11 +593,12 @@ export class GlideClient extends BaseClient { * ``` */ public async functionLoad( - libraryCode: string, - replace?: boolean, - ): Promise { + libraryCode: GlideString, + options?: { replace?: boolean } & DecoderOption, + ): Promise { return this.createWritePromise( - createFunctionLoad(libraryCode, replace), + createFunctionLoad(libraryCode, options?.replace), + { decoder: options?.decoder }, ); } @@ -603,8 +608,8 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @returns A simple OK response. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -612,8 +617,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 'OK' * ``` */ - public async functionFlush(mode?: FlushMode): Promise { - return this.createWritePromise(createFunctionFlush(mode)); + public async functionFlush(mode?: FlushMode): Promise<"OK"> { + return this.createWritePromise(createFunctionFlush(mode), { + decoder: Decoder.String, + }); } /** @@ -622,7 +629,7 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param options - Parameters to filter and request additional info. + * @param options - (Optional) See {@link FunctionListOptions} and {@link DecoderOption}. * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. * * @example @@ -645,9 +652,11 @@ export class GlideClient extends BaseClient { * ``` */ public async functionList( - options?: FunctionListOptions, + options?: FunctionListOptions & DecoderOption, ): Promise { - return this.createWritePromise(createFunctionList(options)); + return this.createWritePromise(createFunctionList(options), { + decoder: options?.decoder, + }); } /** @@ -660,6 +669,8 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A Record where the key is the node address and the value is a Record with two keys: * - `"running_script"`: Information about the running script, or `null` if no script is running. * - `"engines"`: Information about available engines and their stats. @@ -694,8 +705,12 @@ export class GlideClient extends BaseClient { * // } * ``` */ - public async functionStats(): Promise { - return this.createWritePromise(createFunctionStats()); + public async functionStats( + decoder?: Decoder, + ): Promise { + return this.createWritePromise(createFunctionStats(), { + decoder, + }); } /** @@ -706,14 +721,16 @@ export class GlideClient extends BaseClient { * @see {@link https://valkey.io/commands/function-kill/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @returns `OK` if function is terminated. Otherwise, throws an error. + * @returns `"OK"` if function is terminated. Otherwise, throws an error. * @example * ```typescript * await client.functionKill(); * ``` */ public async functionKill(): Promise<"OK"> { - return this.createWritePromise(createFunctionKill()); + return this.createWritePromise(createFunctionKill(), { + decoder: Decoder.String, + }); } /** diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 193d49b7d5..967ea0e568 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -798,8 +798,7 @@ export class GlideClusterClient extends BaseClient { * * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. - * @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`. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * @returns The invoked function's return value. * * @example @@ -809,12 +808,13 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async fcallWithRoute( - func: string, - args: string[], - route?: Routes, + func: GlideString, + args: GlideString[], + options?: RouteOption & DecoderOption, ): Promise> { return this.createWritePromise(createFCall(func, [], args), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } @@ -826,8 +826,7 @@ export class GlideClusterClient extends BaseClient { * * @param func - The function name. * @param args - A list of `function` arguments and it should not represent names of keys. - * @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`. + * @param options - (Optional) See {@link RouteOption} and {@link DecoderOption}. * @returns The invoked function's return value. * * @example @@ -838,12 +837,13 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async fcallReadonlyWithRoute( - func: string, - args: string[], - route?: Routes, + func: GlideString, + args: GlideString[], + options?: RouteOption & DecoderOption, ): Promise> { return this.createWritePromise(createFCallReadOnly(func, [], args), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } @@ -854,9 +854,9 @@ export class GlideClusterClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The library name to delete. - * @param route - The command will be routed to all primary node, unless `route` is provided, in which + * @param route - (Optional) The command will be routed to all primary node, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. - * @returns A simple OK response. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -865,11 +865,12 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async functionDelete( - libraryCode: string, + libraryCode: GlideString, route?: Routes, - ): Promise { + ): Promise<"OK"> { return this.createWritePromise(createFunctionDelete(libraryCode), { route: toProtobufRoute(route), + decoder: Decoder.String, }); } @@ -880,10 +881,11 @@ export class GlideClusterClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. - * @param replace - Whether the given library should overwrite a library with the same name if it + * @param options - (Optional) Additional parameters: + * - (Optional) `replace`: whether the given library should overwrite a library with the same name if it * already exists. - * @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`. + * - (Optional) `route`: see {@link RouteOption}. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns The library name that was loaded. * * @example @@ -894,13 +896,18 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async functionLoad( - libraryCode: string, - replace?: boolean, - route?: Routes, - ): Promise { + libraryCode: GlideString, + options?: { + replace?: boolean; + } & RouteOption & + DecoderOption, + ): Promise { return this.createWritePromise( - createFunctionLoad(libraryCode, replace), - { route: toProtobufRoute(route) }, + createFunctionLoad(libraryCode, options?.replace), + { + route: toProtobufRoute(options?.route), + decoder: options?.decoder, + }, ); } @@ -910,10 +917,10 @@ export class GlideClusterClient extends BaseClient { * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. - * @returns A simple OK response. + * @param options - (Optional) Additional parameters: + * - (Optional) `mode`: the flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * - (Optional) `route`: see {@link RouteOption}. + * @returns A simple `"OK"` response. * * @example * ```typescript @@ -922,11 +929,13 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async functionFlush( - mode?: FlushMode, - route?: Routes, - ): Promise { - return this.createWritePromise(createFunctionFlush(mode), { - route: toProtobufRoute(route), + options?: { + mode?: FlushMode; + } & RouteOption, + ): Promise<"OK"> { + return this.createWritePromise(createFunctionFlush(options?.mode), { + route: toProtobufRoute(options?.route), + decoder: Decoder.String, }); } @@ -936,9 +945,7 @@ export class GlideClusterClient extends BaseClient { * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param options - Parameters to filter and request additional info. - * @param route - The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed to a random node. + * @param options - (Optional) See {@link FunctionListOptions}, {@link DecoderOption}, and {@link RouteOption}. * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. * * @example @@ -961,25 +968,22 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async functionList( - options?: FunctionListOptions, - route?: Routes, + options?: FunctionListOptions & DecoderOption & RouteOption, ): Promise> { return this.createWritePromise(createFunctionList(options), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } /** * Returns information about the function that's currently running and information about the * available execution engines. - * - * * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param route - The command will be routed automatically to all nodes, unless `route` is provided, in which - * case the client will route the command to the nodes defined by `route`. + * @param options - (Optional) See {@link DecoderOption} and {@link RouteOption}. * @returns A `Record` with two keys: * - `"running_script"` with information about the running script. * - `"engines"` with information about available engines and their stats. @@ -1020,10 +1024,11 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async functionStats( - route?: Routes, + options?: RouteOption & DecoderOption, ): Promise> { return this.createWritePromise(createFunctionStats(), { - route: toProtobufRoute(route), + route: toProtobufRoute(options?.route), + decoder: options?.decoder, }); } @@ -1036,7 +1041,7 @@ export class GlideClusterClient extends BaseClient { * * @param route - (Optional) The client will route the command to the nodes defined by `route`. * If not defined, the command will be routed to all nodes. - * @returns `OK` if function is terminated. Otherwise, throws an error. + * @returns `"OK"` if function is terminated. Otherwise, throws an error. * * @example * ```typescript @@ -1046,6 +1051,7 @@ export class GlideClusterClient extends BaseClient { public async functionKill(route?: Routes): Promise<"OK"> { return this.createWritePromise(createFunctionKill(), { route: toProtobufRoute(route), + decoder: Decoder.String, }); } @@ -1069,8 +1075,8 @@ export class GlideClusterClient extends BaseClient { route?: Routes, ): Promise> { return this.createWritePromise(createFunctionDump(), { - decoder: Decoder.Bytes, route: toProtobufRoute(route), + decoder: Decoder.Bytes, }); } @@ -1081,10 +1087,10 @@ export class GlideClusterClient extends BaseClient { * @remarks Since Valkey version 7.0.0. * * @param payload - The serialized data from {@link functionDump}. - * @param policy - (Optional) A policy for handling existing libraries, see {@link FunctionRestorePolicy}. + * @param options - (Optional) Additional parameters: + * - (Optional) `policy`: a policy for handling existing libraries, see {@link FunctionRestorePolicy}. * {@link FunctionRestorePolicy.APPEND} is used by default. - * @param route - (Optional) The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed all primary nodes. + * - (Optional) `route`: see {@link RouteOption}. * @returns `"OK"`. * * @example @@ -1094,11 +1100,14 @@ export class GlideClusterClient extends BaseClient { */ public async functionRestore( payload: Buffer, - options?: { policy?: FunctionRestorePolicy; route?: Routes }, + options?: { policy?: FunctionRestorePolicy } & RouteOption, ): Promise<"OK"> { return this.createWritePromise( createFunctionRestore(payload, options?.policy), - { decoder: Decoder.String, route: toProtobufRoute(options?.route) }, + { + route: toProtobufRoute(options?.route), + decoder: Decoder.String, + }, ); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0b9765b281..ffbca862f9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3189,7 +3189,11 @@ export class BaseTransaction> { * * Command Response - The invoked function's return value. */ - public fcall(func: string, keys: string[], args: string[]): T { + public fcall( + func: GlideString, + keys: GlideString[], + args: GlideString[], + ): T { return this.addAndReturn(createFCall(func, keys, args)); } @@ -3206,7 +3210,11 @@ export class BaseTransaction> { * * Command Response - The invoked function's return value. */ - public fcallReadonly(func: string, keys: string[], args: string[]): T { + public fcallReadonly( + func: GlideString, + keys: GlideString[], + args: GlideString[], + ): T { return this.addAndReturn(createFCallReadOnly(func, keys, args)); } @@ -3218,9 +3226,9 @@ export class BaseTransaction> { * * @param libraryCode - The library name to delete. * - * Command Response - `OK`. + * Command Response - `"OK"`. */ - public functionDelete(libraryCode: string): T { + public functionDelete(libraryCode: GlideString): T { return this.addAndReturn(createFunctionDelete(libraryCode)); } @@ -3231,12 +3239,12 @@ export class BaseTransaction> { * @remarks Since Valkey version 7.0.0. * * @param libraryCode - The source code that implements the library. - * @param replace - Whether the given library should overwrite a library with the same name if it + * @param replace - (Optional) Whether the given library should overwrite a library with the same name if it * already exists. * * Command Response - The library name that was loaded. */ - public functionLoad(libraryCode: string, replace?: boolean): T { + public functionLoad(libraryCode: GlideString, replace?: boolean): T { return this.addAndReturn(createFunctionLoad(libraryCode, replace)); } @@ -3246,8 +3254,8 @@ export class BaseTransaction> { * @see {@link https://valkey.io/commands/function-flush/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * Command Response - `OK`. + * @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * Command Response - `"OK"`. */ public functionFlush(mode?: FlushMode): T { return this.addAndReturn(createFunctionFlush(mode)); @@ -3259,7 +3267,7 @@ export class BaseTransaction> { * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * - * @param options - Parameters to filter and request additional info. + * @param options - (Optional) Parameters to filter and request additional info. * * Command Response - Info about all or selected libraries and their functions in {@link FunctionListResponse} format. */ diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index f589f4d63f..f18624d261 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -661,14 +661,26 @@ describe("GlideClient", () => { ); expect(await client.functionList()).toEqual([]); - expect(await client.functionLoad(code)).toEqual(libName); + expect(await client.functionLoad(Buffer.from(code))).toEqual( + libName, + ); expect( - await client.fcall(funcName, [], ["one", "two"]), - ).toEqual("one"); + await client.fcall( + Buffer.from(funcName), + [], + [Buffer.from("one"), "two"], + Decoder.Bytes, + ), + ).toEqual(Buffer.from("one")); expect( - await client.fcallReadonly(funcName, [], ["one", "two"]), - ).toEqual("one"); + await client.fcallReadonly( + Buffer.from(funcName), + [], + ["one", Buffer.from("two")], + Decoder.Bytes, + ), + ).toEqual(Buffer.from("one")); let functionStats = await client.functionStats(); @@ -677,7 +689,7 @@ describe("GlideClient", () => { } let functionList = await client.functionList({ - libNamePattern: libName, + libNamePattern: Buffer.from(libName), }); let expectedDescription = new Map([ [funcName, null], @@ -694,13 +706,17 @@ describe("GlideClient", () => { ); // re-load library without replace - await expect(client.functionLoad(code)).rejects.toThrow( `Library '${libName}' already exists`, ); // re-load library with replace - expect(await client.functionLoad(code, true)).toEqual(libName); + expect( + await client.functionLoad(code, { + replace: true, + decoder: Decoder.Bytes, + }), + ).toEqual(Buffer.from(libName)); // overwrite lib with new code const func2Name = "myfunc2c" + uuidv4().replaceAll("-", ""); @@ -712,9 +728,9 @@ describe("GlideClient", () => { ]), true, ); - expect(await client.functionLoad(newCode, true)).toEqual( - libName, - ); + expect( + await client.functionLoad(newCode, { replace: true }), + ).toEqual(libName); functionList = await client.functionList({ withCode: true }); expectedDescription = new Map([ @@ -868,7 +884,9 @@ describe("GlideClient", () => { await expect(client.functionKill()).rejects.toThrow(/notbusy/i); // load the lib - expect(await client.functionLoad(code, true)).toEqual(libName); + expect( + await client.functionLoad(code, { replace: true }), + ).toEqual(libName); try { // call the function without await @@ -940,7 +958,9 @@ describe("GlideClient", () => { await expect(client.functionKill()).rejects.toThrow(/notbusy/i); // load the lib - expect(await client.functionLoad(code, true)).toEqual(libName); + expect( + await client.functionLoad(code, { replace: true }), + ).toEqual(libName); let promise = null; diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 9d4687f69e..d938d20c2b 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -862,18 +862,19 @@ describe("GlideClusterClient", () => { ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; - let functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); + let functionList = await client.functionList({ + libNamePattern: libName, + route: route, + }); checkClusterResponse( functionList as object, singleNodeRoute, (value) => expect(value).toEqual([]), ); - let functionStats = - await client.functionStats(route); + let functionStats = await client.functionStats({ + route: route, + }); checkClusterResponse( functionStats as object, singleNodeRoute, @@ -891,10 +892,10 @@ describe("GlideClusterClient", () => { libName, ); - functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); + functionList = await client.functionList({ + libNamePattern: libName, + route: route, + }); let expectedDescription = new Map< string, string | null @@ -914,8 +915,9 @@ describe("GlideClusterClient", () => { expectedFlags, ), ); - functionStats = - await client.functionStats(route); + functionStats = await client.functionStats({ + route: route, + }); checkClusterResponse( functionStats as object, singleNodeRoute, @@ -932,7 +934,7 @@ describe("GlideClusterClient", () => { let fcall = await client.fcallWithRoute( funcName, ["one", "two"], - route, + { route: route }, ); checkClusterResponse( fcall as object, @@ -942,7 +944,7 @@ describe("GlideClusterClient", () => { fcall = await client.fcallReadonlyWithRoute( funcName, ["one", "two"], - route, + { route: route }, ); checkClusterResponse( fcall as object, @@ -959,7 +961,9 @@ describe("GlideClusterClient", () => { // re-load library with replace expect( - await client.functionLoad(code, true), + await client.functionLoad(code, { + replace: true, + }), ).toEqual(libName); // overwrite lib with new code @@ -974,13 +978,16 @@ describe("GlideClusterClient", () => { true, ); expect( - await client.functionLoad(newCode, true), + await client.functionLoad(newCode, { + replace: true, + }), ).toEqual(libName); - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); + functionList = await client.functionList({ + libNamePattern: libName, + withCode: true, + route: route, + }); expectedDescription = new Map< string, string | null @@ -1005,8 +1012,9 @@ describe("GlideClusterClient", () => { newCode, ), ); - functionStats = - await client.functionStats(route); + functionStats = await client.functionStats({ + route: route, + }); checkClusterResponse( functionStats as object, singleNodeRoute, @@ -1022,7 +1030,7 @@ describe("GlideClusterClient", () => { fcall = await client.fcallWithRoute( func2Name, ["one", "two"], - route, + { route: route }, ); checkClusterResponse( fcall as object, @@ -1033,7 +1041,7 @@ describe("GlideClusterClient", () => { fcall = await client.fcallReadonlyWithRoute( func2Name, ["one", "two"], - route, + { route: route }, ); checkClusterResponse( fcall as object, @@ -1078,8 +1086,7 @@ describe("GlideClusterClient", () => { : "allPrimaries"; const functionList1 = await client.functionList( - {}, - route, + { route: route }, ); checkClusterResponse( functionList1 as object, @@ -1089,25 +1096,23 @@ describe("GlideClusterClient", () => { // load the library expect( - await client.functionLoad( - code, - undefined, - route, - ), + await client.functionLoad(code, { + route: route, + }), ).toEqual(libName); // flush functions expect( - await client.functionFlush( - FlushMode.SYNC, - route, - ), + await client.functionFlush({ + mode: FlushMode.SYNC, + route: route, + }), ).toEqual("OK"); expect( - await client.functionFlush( - FlushMode.ASYNC, - route, - ), + await client.functionFlush({ + mode: FlushMode.ASYNC, + route: route, + }), ).toEqual("OK"); const functionList2 = @@ -1120,11 +1125,9 @@ describe("GlideClusterClient", () => { // Attempt to re-load library without overwriting to ensure FLUSH was effective expect( - await client.functionLoad( - code, - undefined, - route, - ), + await client.functionLoad(code, { + route: route, + }), ).toEqual(libName); } finally { expect(await client.functionFlush()).toEqual( @@ -1162,10 +1165,9 @@ describe("GlideClusterClient", () => { const route: Routes = singleNodeRoute ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; - let functionList = await client.functionList( - {}, - route, - ); + let functionList = await client.functionList({ + route: route, + }); checkClusterResponse( functionList as object, singleNodeRoute, @@ -1173,11 +1175,9 @@ describe("GlideClusterClient", () => { ); // load the library expect( - await client.functionLoad( - code, - undefined, - route, - ), + await client.functionLoad(code, { + route: route, + }), ).toEqual(libName); // Delete the function @@ -1185,10 +1185,11 @@ describe("GlideClusterClient", () => { await client.functionDelete(libName, route), ).toEqual("OK"); - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); + functionList = await client.functionList({ + libNamePattern: libName, + withCode: true, + route: route, + }); checkClusterResponse( functionList as object, singleNodeRoute, @@ -1251,17 +1252,18 @@ describe("GlideClusterClient", () => { // load the lib expect( - await client.functionLoad( - code, - true, - route, - ), + await client.functionLoad(code, { + replace: true, + route: route, + }), ).toEqual(libName); try { // call the function without await const promise = testClient - .fcallWithRoute(funcName, [], route) + .fcallWithRoute(funcName, [], { + route: route, + }) .catch((e) => expect( (e as Error).message, @@ -1323,7 +1325,10 @@ describe("GlideClusterClient", () => { ? { type: "primarySlotKey", key: "1" } : "allPrimaries"; expect( - await client.functionFlush(FlushMode.SYNC, route), + await client.functionFlush({ + mode: FlushMode.SYNC, + route: route, + }), ).toEqual("OK"); try { @@ -1351,17 +1356,15 @@ describe("GlideClusterClient", () => { false, ); expect( - await client.functionLoad( - code, - undefined, - route, - ), + await client.functionLoad(code, { + route: route, + }), ).toEqual(name1); - const flist = await client.functionList( - { withCode: true }, - route, - ); + const flist = await client.functionList({ + withCode: true, + route: route, + }); response = await client.functionDump(route); const dump = ( singleNodeRoute @@ -1395,18 +1398,18 @@ describe("GlideClusterClient", () => { ).toEqual("OK"); // but nothing changed - all code overwritten expect( - await client.functionList( - { withCode: true }, - route, - ), + await client.functionList({ + withCode: true, + route: route, + }), ).toEqual(flist); // create lib with another name, but with the same function names expect( - await client.functionFlush( - FlushMode.SYNC, - route, - ), + await client.functionFlush({ + mode: FlushMode.SYNC, + route: route, + }), ).toEqual("OK"); code = generateLuaLibCode( name2, @@ -1417,11 +1420,9 @@ describe("GlideClusterClient", () => { false, ); expect( - await client.functionLoad( - code, - undefined, - route, - ), + await client.functionLoad(code, { + route: route, + }), ).toEqual(name2); // REPLACE policy now fails due to a name collision @@ -1441,17 +1442,17 @@ describe("GlideClusterClient", () => { }), ).toEqual("OK"); expect( - await client.functionList( - { withCode: true }, - route, - ), + await client.functionList({ + withCode: true, + route: route, + }), ).toEqual(flist); // call restored functions let res = await client.fcallWithRoute( name1, ["meow", "woem"], - route, + { route: route }, ); if (singleNodeRoute) { @@ -1465,7 +1466,7 @@ describe("GlideClusterClient", () => { res = await client.fcallWithRoute( name2, ["meow", "woem"], - route, + { route: route }, ); if (singleNodeRoute) { @@ -1523,7 +1524,10 @@ describe("GlideClusterClient", () => { // load the lib expect( - await client.functionLoad(code, true, route), + await client.functionLoad(code, { + replace: true, + route: route, + }), ).toEqual(libName); let promise = null; @@ -1600,7 +1604,10 @@ describe("GlideClusterClient", () => { false, ); expect( - await client.functionLoad(code, true, route), + await client.functionLoad(code, { + replace: true, + route: route, + }), ).toEqual(name1); // Verify functionDump diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 0b177e62f0..ba8e185232 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -462,7 +462,7 @@ export function checkFunctionListResponse( */ export function checkFunctionStatsResponse( response: FunctionStatsSingleResponse, - runningFunction: string[], + runningFunction: GlideString[], libCount: number, functionCount: number, ) { From ed9a7afb24425e506d78850e7f21619047d71515 Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Mon, 26 Aug 2024 13:53:09 -0700 Subject: [PATCH 223/236] Node binary api: blmpop, lrem, lset, ltrim and rpushx commands updated for binary command inputs (#2195) Node binary api: blmpop, lrem, lset, ltrim and rpushx command Signed-off-by: Prateek Kumar --- node/src/BaseClient.ts | 31 +++++++---- node/src/Commands.ts | 20 +++---- node/src/Transaction.ts | 12 ++--- node/tests/SharedTests.ts | 110 ++++++++++++++++++++++++++++++++++---- 4 files changed, 138 insertions(+), 35 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5e8989e4c7..a19be05ae2 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2338,11 +2338,13 @@ export class BaseClient { * ``` */ public async lset( - key: string, + key: GlideString, index: number, - element: string, + element: GlideString, ): Promise<"OK"> { - return this.createWritePromise(createLSet(key, index, element)); + return this.createWritePromise(createLSet(key, index, element), { + decoder: Decoder.String, + }); } /** Trim an existing list so that it will contain only the specified range of elements specified. @@ -2367,8 +2369,14 @@ export class BaseClient { * console.log(result); // Output: 'OK' - Indicates that the list has been trimmed to contain elements from 0 to 1. * ``` */ - public async ltrim(key: string, start: number, end: number): Promise<"OK"> { - return this.createWritePromise(createLTrim(key, start, end)); + public async ltrim( + key: GlideString, + start: number, + end: number, + ): Promise<"OK"> { + return this.createWritePromise(createLTrim(key, start, end), { + decoder: Decoder.String, + }); } /** Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. @@ -2390,9 +2398,9 @@ export class BaseClient { * ``` */ public async lrem( - key: string, + key: GlideString, count: number, - element: string, + element: GlideString, ): Promise { return this.createWritePromise(createLRem(key, count, element)); } @@ -2443,7 +2451,10 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that the list has two elements. * ``` * */ - public async rpushx(key: string, elements: string[]): Promise { + public async rpushx( + key: GlideString, + elements: GlideString[], + ): Promise { return this.createWritePromise(createRPushX(key, elements)); } @@ -6506,13 +6517,13 @@ export class BaseClient { * ``` */ public async blmpop( - keys: string[], + keys: GlideString[], direction: ListDirection, timeout: number, count?: number, ): Promise> { return this.createWritePromise( - createBLMPop(timeout, keys, direction, count), + createBLMPop(keys, direction, timeout, count), ); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index a3ed23a5bd..b4a6f5e236 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -942,9 +942,9 @@ export function createBLMove( * @internal */ export function createLSet( - key: string, + key: GlideString, index: number, - element: string, + element: GlideString, ): command_request.Command { return createCommand(RequestType.LSet, [key, index.toString(), element]); } @@ -953,7 +953,7 @@ export function createLSet( * @internal */ export function createLTrim( - key: string, + key: GlideString, start: number, end: number, ): command_request.Command { @@ -968,9 +968,9 @@ export function createLTrim( * @internal */ export function createLRem( - key: string, + key: GlideString, count: number, - element: string, + element: GlideString, ): command_request.Command { return createCommand(RequestType.LRem, [key, count.toString(), element]); } @@ -989,8 +989,8 @@ export function createRPush( * @internal */ export function createRPushX( - key: string, - elements: string[], + key: GlideString, + elements: GlideString[], ): command_request.Command { return createCommand(RequestType.RPushX, [key].concat(elements)); } @@ -3829,12 +3829,12 @@ export function createLMPop( * @internal */ export function createBLMPop( - timeout: number, - keys: string[], + keys: GlideString[], direction: ListDirection, + timeout: number, count?: number, ): command_request.Command { - const args: string[] = [ + const args: GlideString[] = [ timeout.toString(), keys.length.toString(), ...keys, diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index ffbca862f9..62408d11ed 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1190,7 +1190,7 @@ export class BaseTransaction> { * * Command Response - Always "OK". */ - public lset(key: string, index: number, element: string): T { + public lset(key: GlideString, index: number, element: GlideString): T { return this.addAndReturn(createLSet(key, index, element)); } @@ -1209,7 +1209,7 @@ export class BaseTransaction> { * If `end` exceeds the actual end of the list, it will be treated like the last element of the list. * If `key` does not exist the command will be ignored. */ - public ltrim(key: string, start: number, end: number): T { + public ltrim(key: GlideString, start: number, end: number): T { return this.addAndReturn(createLTrim(key, start, end)); } @@ -1225,7 +1225,7 @@ export class BaseTransaction> { * Command Response - the number of the removed elements. * If `key` does not exist, 0 is returned. */ - public lrem(key: string, count: number, element: string): T { + public lrem(key: GlideString, count: number, element: string): T { return this.addAndReturn(createLRem(key, count, element)); } @@ -1254,7 +1254,7 @@ export class BaseTransaction> { * * Command Response - The length of the list after the push operation. */ - public rpushx(key: string, elements: string[]): T { + public rpushx(key: GlideString, elements: GlideString[]): T { return this.addAndReturn(createRPushX(key, elements)); } @@ -3810,12 +3810,12 @@ export class BaseTransaction> { * If no member could be popped and the timeout expired, returns `null`. */ public blmpop( - keys: string[], + keys: GlideString[], direction: ListDirection, timeout: number, count?: number, ): T { - return this.addAndReturn(createBLMPop(timeout, keys, direction, count)); + return this.addAndReturn(createBLMPop(keys, direction, timeout, count)); } /** diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 645a37dd6d..3f315d7af5 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2078,7 +2078,6 @@ export function runBaseTests(config: { 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([ @@ -2471,6 +2470,17 @@ export function runBaseTests(config: { await expect(client.lset(nonListKey, 0, "b")).rejects.toThrow( RequestError, ); + + //test lset for binary key and element values + const key2 = uuidv4(); + expect(await client.lpush(key2, lpushArgs)).toEqual(4); + // assert lset result + expect( + await client.lset(Buffer.from(key2), index, element), + ).toEqual("OK"); + expect(await client.lrange(key2, 0, negativeIndex)).toEqual( + expectedList, + ); }, protocol); }, config.timeout, @@ -2502,6 +2512,17 @@ export function runBaseTests(config: { "Operation against a key holding the wrong kind of value", ); } + + //test for binary key as input to the command + const key2 = uuidv4(); + expect(await client.lpush(key2, valueList)).toEqual(4); + expect(await client.ltrim(Buffer.from(key2), 0, 1)).toEqual( + "OK", + ); + expect(await client.lrange(key2, 0, -1)).toEqual([ + "value1", + "value2", + ]); }, protocol); }, config.timeout, @@ -2511,7 +2532,7 @@ export function runBaseTests(config: { `lrem with existing key and non existing key_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); + const key1 = uuidv4(); const valueList = [ "value1", "value2", @@ -2519,23 +2540,39 @@ export function runBaseTests(config: { "value1", "value2", ]; - expect(await client.lpush(key, valueList)).toEqual(5); - expect(await client.lrem(key, 2, "value1")).toEqual(2); - expect(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.lpush(key1, valueList)).toEqual(5); + expect(await client.lrem(key1, 2, "value1")).toEqual(2); + expect(await client.lrange(key1, 0, -1)).toEqual([ "value2", "value2", "value1", ]); - expect(await client.lrem(key, -1, "value2")).toEqual(1); - expect(await client.lrange(key, 0, -1)).toEqual([ + expect(await client.lrem(key1, -1, "value2")).toEqual(1); + expect(await client.lrange(key1, 0, -1)).toEqual([ "value2", "value1", ]); - expect(await client.lrem(key, 0, "value2")).toEqual(1); - expect(await client.lrange(key, 0, -1)).toEqual(["value1"]); + expect(await client.lrem(key1, 0, "value2")).toEqual(1); + expect(await client.lrange(key1, 0, -1)).toEqual(["value1"]); expect(await client.lrem("nonExistingKey", 2, "value")).toEqual( 0, ); + + // test for binary key and element as input to the command + const key2 = uuidv4(); + expect(await client.lpush(key2, valueList)).toEqual(5); + expect( + await client.lrem( + Buffer.from(key2), + 2, + Buffer.from("value1"), + ), + ).toEqual(2); + expect(await client.lrange(key2, 0, -1)).toEqual([ + "value2", + "value2", + "value1", + ]); }, protocol); }, config.timeout, @@ -2633,6 +2670,23 @@ export function runBaseTests(config: { await expect(client.rpushx(key2, [])).rejects.toThrow( RequestError, ); + + //test for binary key and elemnts as inputs to the command. + const key4 = uuidv4(); + expect(await client.rpush(key4, ["0"])).toEqual(1); + expect( + await client.rpushx(Buffer.from(key4), [ + Buffer.from("1"), + Buffer.from("2"), + Buffer.from("3"), + ]), + ).toEqual(4); + expect(await client.lrange(key4, 0, -1)).toEqual([ + "0", + "1", + "2", + "3", + ]); }, protocol); }, config.timeout, @@ -10629,6 +10683,44 @@ export function runBaseTests(config: { await expect( client.blmpop([nonListKey], ListDirection.RIGHT, 0.1, 1), ).rejects.toThrow(RequestError); + + // Test with single binary key array as input + const key3 = "{key}" + uuidv4(); + const singleKeyArrayWithKey3 = [Buffer.from(key3)]; + + // pushing to the arrays to be popped + expect(await client.lpush(key3, lpushArgs)).toEqual(5); + const expectedWithKey3 = { [key3]: ["five"] }; + + // checking correct result from popping + expect( + await client.blmpop( + singleKeyArrayWithKey3, + ListDirection.LEFT, + 0.1, + ), + ).toEqual(expectedWithKey3); + + // test with multiple binary keys array as input + const key4 = "{key}" + uuidv4(); + const multiKeyArrayWithKey3AndKey4 = [ + Buffer.from(key4), + Buffer.from(key3), + ]; + + // pushing to the arrays to be popped + expect(await client.lpush(key4, lpushArgs)).toEqual(5); + const expectedWithKey4 = { [key4]: ["one", "two"] }; + + // checking correct result from popping + expect( + await client.blmpop( + multiKeyArrayWithKey3AndKey4, + ListDirection.RIGHT, + 0.1, + 2, + ), + ).toEqual(expectedWithKey4); }, protocol); }, config.timeout, From 6c7ac8183514a5f6fbd17f7356e4b45d36248bed Mon Sep 17 00:00:00 2001 From: ort-bot Date: Tue, 27 Aug 2024 00:22:30 +0000 Subject: [PATCH 224/236] Updated attribution files Signed-off-by: ort-bot --- python/THIRD_PARTY_LICENSES_PYTHON | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index 65bd19e880..e3c0129045 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -36764,7 +36764,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: googleapis-common-protos:1.63.2 +Package: googleapis-common-protos:1.64.0 The following copyrights and licenses were found in the source code of this package: From e6d2ee53478d1c61a5f5267e82e860a4a709374a Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Tue, 27 Aug 2024 15:11:05 +0300 Subject: [PATCH 225/236] Node: Add binary support to spop spopcount sadd srem smembers (#2181) * add binary support to spop spopcount sadd srem smembers Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 45 ++++++++++++++++++++++++++++---------- node/src/Commands.ts | 15 +++++++------ node/src/Transaction.ts | 10 ++++----- node/tests/SharedTests.ts | 46 +++++++++++++++++++++++++++++++++++---- 4 files changed, 88 insertions(+), 28 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a19be05ae2..e72aea2ffb 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2541,7 +2541,10 @@ export class BaseClient { * console.log(result); // Output: 2 * ``` */ - public async sadd(key: string, members: string[]): Promise { + public async sadd( + key: GlideString, + members: GlideString[], + ): Promise { return this.createWritePromise(createSAdd(key, members)); } @@ -2561,7 +2564,10 @@ export class BaseClient { * console.log(result); // Output: 2 * ``` */ - public async srem(key: string, members: string[]): Promise { + public async srem( + key: GlideString, + members: GlideString[], + ): Promise { return this.createWritePromise(createSRem(key, members)); } @@ -2614,6 +2620,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/smembers/|valkey.io} for details. * * @param key - The key to return its members. + * @param options - (Optional) See {@link DecoderOption}. * @returns A `Set` containing all members of the set. * If `key` does not exist, it is treated as an empty set and this command returns an empty `Set`. * @@ -2624,10 +2631,14 @@ export class BaseClient { * console.log(result); // Output: Set {'member1', 'member2', 'member3'} * ``` */ - public async smembers(key: string): Promise> { - return this.createWritePromise(createSMembers(key)).then( - (smembes) => new Set(smembes), - ); + public async smembers( + key: GlideString, + options?: DecoderOption, + ): Promise> { + return this.createWritePromise( + createSMembers(key), + options, + ).then((smembers) => new Set(smembers)); } /** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. @@ -2910,6 +2921,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/spop/|valkey.io} for details. * * @param key - The key of the set. + * @param options - (Optional) See {@link DecoderOption}. * @returns the value of the popped member. * If `key` does not exist, null will be returned. * @@ -2927,8 +2939,11 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public async spop(key: string): Promise { - return this.createWritePromise(createSPop(key)); + public async spop( + key: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createSPop(key), options); } /** Removes and returns up to `count` random members from the set value store at `key`, depending on the set's length. @@ -2937,6 +2952,7 @@ export class BaseClient { * * @param key - The key of the set. * @param count - The count of the elements to pop from the set. + * @param options - (Optional) See {@link DecoderOption}. * @returns A `Set` containing the popped elements, depending on the set's length. * If `key` does not exist, an empty `Set` will be returned. * @@ -2954,10 +2970,15 @@ export class BaseClient { * console.log(result); // Output: Set {} - An empty set is returned since the key does not exist. * ``` */ - public async spopCount(key: string, count: number): Promise> { - return this.createWritePromise(createSPop(key, count)).then( - (spop) => new Set(spop), - ); + public async spopCount( + key: GlideString, + count: number, + options?: DecoderOption, + ): Promise> { + return this.createWritePromise( + createSPop(key, count), + options, + ).then((spop) => new Set(spop)); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b4a6f5e236..2f657211c2 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1011,8 +1011,8 @@ export function createRPop( * @internal */ export function createSAdd( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): command_request.Command { return createCommand(RequestType.SAdd, [key].concat(members)); } @@ -1021,8 +1021,8 @@ export function createSAdd( * @internal */ export function createSRem( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): command_request.Command { return createCommand(RequestType.SRem, [key].concat(members)); } @@ -1047,7 +1047,7 @@ export function createSScan( /** * @internal */ -export function createSMembers(key: string): command_request.Command { +export function createSMembers(key: GlideString): command_request.Command { return createCommand(RequestType.SMembers, [key]); } @@ -1162,10 +1162,11 @@ export function createSMIsMember( * @internal */ export function createSPop( - key: string, + key: GlideString, count?: number, ): command_request.Command { - const args: string[] = count == undefined ? [key] : [key, count.toString()]; + const args: GlideString[] = + count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.SPop, args); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 62408d11ed..64db80699d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1293,7 +1293,7 @@ export class BaseTransaction> { * * Command Response - the number of members that were added to the set, not including all the members already present in the set. */ - public sadd(key: string, members: string[]): T { + public sadd(key: GlideString, members: GlideString[]): T { return this.addAndReturn(createSAdd(key, members)); } @@ -1306,7 +1306,7 @@ export class BaseTransaction> { * Command Response - the number of members that were removed from the set, not including non existing members. * If `key` does not exist, it is treated as an empty set and this command returns 0. */ - public srem(key: string, members: string[]): T { + public srem(key: GlideString, members: GlideString[]): T { return this.addAndReturn(createSRem(key, members)); } @@ -1334,7 +1334,7 @@ export class BaseTransaction> { * Command Response - all members of the set. * If `key` does not exist, it is treated as an empty set and this command returns empty list. */ - public smembers(key: string): T { + public smembers(key: GlideString): T { return this.addAndReturn(createSMembers(key), true); } @@ -1498,7 +1498,7 @@ export class BaseTransaction> { * Command Response - the value of the popped member. * If `key` does not exist, null will be returned. */ - public spop(key: string): T { + public spop(key: GlideString): T { return this.addAndReturn(createSPop(key)); } @@ -1511,7 +1511,7 @@ export class BaseTransaction> { * Command Response - A list of popped elements will be returned depending on the set's length. * If `key` does not exist, empty list will be returned. */ - public spopCount(key: string, count: number): T { + public spopCount(key: GlideString, count: number): T { return this.addAndReturn(createSPop(key, count), true); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 3f315d7af5..685e863fcc 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2697,17 +2697,34 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); + const keyEncoded = Buffer.from(key); const valueList = ["member1", "member2", "member3", "member4"]; expect(await client.sadd(key, valueList)).toEqual(4); expect( await client.srem(key, ["member3", "nonExistingMember"]), ).toEqual(1); + /// compare the 2 sets. expect(await client.smembers(key)).toEqual( new Set(["member1", "member2", "member4"]), ); expect(await client.srem(key, ["member1"])).toEqual(1); expect(await client.scard(key)).toEqual(2); + + // with key and members as buffers + expect( + await client.sadd(keyEncoded, [Buffer.from("member5")]), + ).toEqual(1); + expect( + await client.srem(keyEncoded, [Buffer.from("member2")]), + ).toEqual(1); + expect( + await client.smembers(keyEncoded, { + decoder: Decoder.Bytes, + }), + ).toEqual( + new Set([Buffer.from("member4"), Buffer.from("member5")]), + ); }, protocol); }, config.timeout, @@ -3369,20 +3386,41 @@ export function runBaseTests(config: { `spop and spopCount test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); + const key1 = uuidv4(); + const key2 = uuidv4(); + const key2Encoded = Buffer.from(key2); let members = ["member1", "member2", "member3"]; - expect(await client.sadd(key, members)).toEqual(3); + let members2 = ["member1", "member2", "member3"]; + + expect(await client.sadd(key1, members)).toEqual(3); + expect(await client.sadd(key2, members2)).toEqual(3); - const result1 = await client.spop(key); + const result1 = await client.spop(key1); expect(members).toContain(result1); members = members.filter((item) => item != result1); - const result2 = await client.spopCount(key, 2); + const result2 = await client.spopCount(key1, 2); expect(result2).toEqual(new Set(members)); expect(await client.spop("nonExistingKey")).toEqual(null); expect(await client.spopCount("nonExistingKey", 1)).toEqual( new Set(), ); + + // with keys and return values as buffers + const result3 = await client.spop(key2Encoded, { + decoder: Decoder.Bytes, + }); + expect(members2).toContain(result3?.toString()); + + members2 = members2.filter( + (item) => item != result3?.toString(), + ); + const result4 = await client.spopCount(key2Encoded, 2, { + decoder: Decoder.Bytes, + }); + expect(result4).toEqual( + new Set(members2.map((item) => Buffer.from(item))), + ); }, protocol); }, config.timeout, From 0b0b7a592eb2a3ba0ca2396956b9902428fd173e Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Tue, 27 Aug 2024 15:30:57 +0300 Subject: [PATCH 226/236] Node: Add binary support to set commands (#2188) * add binary support to sdiff sdiffstore sinter sintercard sinterstore --------- Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 39 ++++++++++++++++++++---------- node/src/Commands.ts | 16 ++++++------- node/src/Transaction.ts | 10 ++++---- node/tests/SharedTests.ts | 50 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e72aea2ffb..7d0fe8cb53 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2692,6 +2692,7 @@ export class BaseClient { * @remarks When in cluster mode, all `keys` must map to the same hash slot. * * @param keys - The `keys` of the sets to get the intersection. + * @param options - (Optional) See {@link DecoderOption}. * @returns - A set of members which are present in all given sets. * If one or more sets do not exist, an empty set will be returned. * @@ -2709,10 +2710,14 @@ export class BaseClient { * console.log(result); // Output: Set {} - An empty set is returned since the key does not exist. * ``` */ - public async sinter(keys: string[]): Promise> { - return this.createWritePromise(createSInter(keys)).then( - (sinter) => new Set(sinter), - ); + public async sinter( + keys: GlideString[], + options?: DecoderOption, + ): Promise> { + return this.createWritePromise( + createSInter(keys), + options, + ).then((sinter) => new Set(sinter)); } /** @@ -2737,7 +2742,10 @@ export class BaseClient { * console.log(result2); // Output: 1 - The computation stops early as the intersection cardinality reaches the limit of 1. * ``` */ - public async sintercard(keys: string[], limit?: number): Promise { + public async sintercard( + keys: GlideString[], + limit?: number, + ): Promise { return this.createWritePromise(createSInterCard(keys, limit)); } @@ -2758,8 +2766,8 @@ export class BaseClient { * ``` */ public async sinterstore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): Promise { return this.createWritePromise(createSInterStore(destination, keys)); } @@ -2771,6 +2779,7 @@ export class BaseClient { * @remarks When in cluster mode, all `keys` must map to the same hash slot. * * @param keys - The keys of the sets to diff. + * @param options - (Optional) See {@link DecoderOption}. * @returns A `Set` of elements representing the difference between the sets. * If a key in `keys` does not exist, it is treated as an empty set. * @@ -2782,10 +2791,14 @@ export class BaseClient { * console.log(result); // Output: Set {"member1"} - "member2" is in "set1" but not "set2" * ``` */ - public async sdiff(keys: string[]): Promise> { - return this.createWritePromise(createSDiff(keys)).then( - (sdiff) => new Set(sdiff), - ); + public async sdiff( + keys: GlideString[], + options?: DecoderOption, + ): Promise> { + return this.createWritePromise( + createSDiff(keys), + options, + ).then((sdiff) => new Set(sdiff)); } /** @@ -2807,8 +2820,8 @@ export class BaseClient { * ``` */ public async sdiffstore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): Promise { return this.createWritePromise(createSDiffStore(destination, keys)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 2f657211c2..aa9ae2cc84 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1073,7 +1073,7 @@ export function createSCard(key: string): command_request.Command { /** * @internal */ -export function createSInter(keys: string[]): command_request.Command { +export function createSInter(keys: GlideString[]): command_request.Command { return createCommand(RequestType.SInter, keys); } @@ -1081,10 +1081,10 @@ export function createSInter(keys: string[]): command_request.Command { * @internal */ export function createSInterCard( - keys: string[], + keys: GlideString[], limit?: number, ): command_request.Command { - let args: string[] = keys; + let args: GlideString[] = keys; args.unshift(keys.length.toString()); if (limit != undefined) { @@ -1098,8 +1098,8 @@ export function createSInterCard( * @internal */ export function createSInterStore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): command_request.Command { return createCommand(RequestType.SInterStore, [destination].concat(keys)); } @@ -1107,7 +1107,7 @@ export function createSInterStore( /** * @internal */ -export function createSDiff(keys: string[]): command_request.Command { +export function createSDiff(keys: GlideString[]): command_request.Command { return createCommand(RequestType.SDiff, keys); } @@ -1115,8 +1115,8 @@ export function createSDiff(keys: string[]): command_request.Command { * @internal */ export function createSDiffStore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): command_request.Command { return createCommand(RequestType.SDiffStore, [destination].concat(keys)); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 64db80699d..c75793df36 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1372,7 +1372,7 @@ export class BaseTransaction> { * Command Response - A set of members which are present in all given sets. * If one or more sets do not exist, an empty set will be returned. */ - public sinter(keys: string[]): T { + public sinter(keys: GlideString[]): T { return this.addAndReturn(createSInter(keys), true); } @@ -1386,7 +1386,7 @@ export class BaseTransaction> { * * Command Response - The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. */ - public sintercard(keys: string[], limit?: number): T { + public sintercard(keys: GlideString[], limit?: number): T { return this.addAndReturn(createSInterCard(keys, limit)); } @@ -1400,7 +1400,7 @@ export class BaseTransaction> { * * Command Response - The number of elements in the resulting set. */ - public sinterstore(destination: string, keys: string[]): T { + public sinterstore(destination: GlideString, keys: GlideString[]): T { return this.addAndReturn(createSInterStore(destination, keys)); } @@ -1414,7 +1414,7 @@ export class BaseTransaction> { * Command Response - A `Set` of elements representing the difference between the sets. * If a key in `keys` does not exist, it is treated as an empty set. */ - public sdiff(keys: string[]): T { + public sdiff(keys: GlideString[]): T { return this.addAndReturn(createSDiff(keys), true); } @@ -1428,7 +1428,7 @@ export class BaseTransaction> { * * Command Response - The number of elements in the resulting set. */ - public sdiffstore(destination: string, keys: string[]): T { + public sdiffstore(destination: GlideString, keys: GlideString[]): T { return this.addAndReturn(createSDiffStore(destination, keys)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 685e863fcc..cd3ae649e1 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2872,6 +2872,14 @@ export function runBaseTests(config: { new Set(["c", "d"]), ); + // positive test case with keys and return value as buffers + expect( + await client.sinter( + [Buffer.from(key1), Buffer.from(key2)], + { decoder: Decoder.Bytes }, + ), + ).toEqual(new Set([Buffer.from("c"), Buffer.from("d")])); + // invalid argument - key list must not be empty try { expect(await client.sinter([])).toThrow(); @@ -2945,6 +2953,14 @@ export function runBaseTests(config: { ), ).toEqual(0); + // with keys as binary buffers + expect( + await client.sintercard([ + Buffer.from(key1), + Buffer.from(key2), + ]), + ).toEqual(3); + // invalid argument - key list must not be empty await expect(client.sintercard([])).rejects.toThrow( RequestError, @@ -3005,6 +3021,17 @@ export function runBaseTests(config: { // overwrite non-set key expect(await client.sinterstore(stringKey, [key2])).toEqual(1); expect(await client.smembers(stringKey)).toEqual(new Set("c")); + + // with destination and keys as binary buffers + expect(await client.sadd(key1, ["a", "b", "c"])); + expect(await client.sadd(key2, ["c", "d", "e"])); + expect( + await client.sinterstore(Buffer.from(key3), [ + Buffer.from(key1), + Buffer.from(key2), + ]), + ).toEqual(1); + expect(await client.smembers(key3)).toEqual(new Set(["c"])); }, protocol); }, config.timeout, @@ -3038,6 +3065,18 @@ export function runBaseTests(config: { new Set(), ); + // key and return value as binary buffers + expect( + await client.sdiff([Buffer.from(key1), Buffer.from(key2)], { + decoder: Decoder.Bytes, + }), + ).toEqual(new Set([Buffer.from("a"), Buffer.from("b")])); + expect( + await client.sdiff([Buffer.from(key2), Buffer.from(key1)], { + decoder: Decoder.Bytes, + }), + ).toEqual(new Set([Buffer.from("d"), Buffer.from("e")])); + // invalid arg - key list must not be empty await expect(client.sdiff([])).rejects.toThrow(); @@ -3110,6 +3149,17 @@ export function runBaseTests(config: { expect(await client.smembers(stringKey)).toEqual( new Set(["a", "b"]), ); + + // with destination and keys as binary buffers + expect( + await client.sdiffstore(Buffer.from(key3), [ + Buffer.from(key1), + Buffer.from(key2), + ]), + ).toEqual(2); + expect(await client.smembers(key3)).toEqual( + new Set(["a", "b"]), + ); }, protocol); }, config.timeout, From 8ebfa398d9170dadafc21ed030b915bc8b05f4d5 Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Tue, 27 Aug 2024 16:33:57 +0300 Subject: [PATCH 227/236] Node: Add binary support to scard smove sismember srandmember (#2197) * add binary support to scard smove sismember smismember srandmember srandmemberCount Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 33 +++++++++++++++++++++------------ node/src/Commands.ts | 21 +++++++++++---------- node/src/Transaction.ts | 16 ++++++++++------ node/tests/SharedTests.ts | 39 +++++++++++++++++++++++++++++++-------- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 7d0fe8cb53..1774f3eb98 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2659,9 +2659,9 @@ export class BaseClient { * ``` */ public async smove( - source: string, - destination: string, - member: string, + source: GlideString, + destination: GlideString, + member: GlideString, ): Promise { return this.createWritePromise( createSMove(source, destination, member), @@ -2682,7 +2682,7 @@ export class BaseClient { * console.log(result); // Output: 3 * ``` */ - public async scard(key: string): Promise { + public async scard(key: GlideString): Promise { return this.createWritePromise(createSCard(key)); } @@ -2900,7 +2900,10 @@ export class BaseClient { * console.log(result); // Output: false - Indicates that "non_existing_member" does not exist in the set "my_set". * ``` */ - public async sismember(key: string, member: string): Promise { + public async sismember( + key: GlideString, + member: GlideString, + ): Promise { return this.createWritePromise(createSIsMember(key, member)); } @@ -2922,8 +2925,8 @@ export class BaseClient { * ``` */ public async smismember( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): Promise { return this.createWritePromise(createSMIsMember(key, members)); } @@ -3000,6 +3003,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/srandmember/|valkey.io} for more details. * * @param key - The key from which to retrieve the set member. + * @param options - (Optional) See {@link DecoderOption}. * @returns A random element from the set, or null if `key` does not exist. * * @example @@ -3016,8 +3020,11 @@ export class BaseClient { * console.log(result); // Output: null * ``` */ - public async srandmember(key: string): Promise { - return this.createWritePromise(createSRandMember(key)); + public async srandmember( + key: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createSRandMember(key), options); } /** @@ -3029,6 +3036,7 @@ export class BaseClient { * @param count - The number of members to return. * If `count` is positive, returns unique members. * If `count` is negative, allows for duplicates members. + * @param options - (Optional) See {@link DecoderOption}. * @returns a list of members from the set. If the set does not exist or is empty, an empty list will be returned. * * @example @@ -3046,10 +3054,11 @@ export class BaseClient { * ``` */ public async srandmemberCount( - key: string, + key: GlideString, count: number, - ): Promise { - return this.createWritePromise(createSRandMember(key, count)); + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createSRandMember(key, count), options); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index aa9ae2cc84..14df8b48cc 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1056,9 +1056,9 @@ export function createSMembers(key: GlideString): command_request.Command { * @internal */ export function createSMove( - source: string, - destination: string, - member: string, + source: GlideString, + destination: GlideString, + member: GlideString, ): command_request.Command { return createCommand(RequestType.SMove, [source, destination, member]); } @@ -1066,7 +1066,7 @@ export function createSMove( /** * @internal */ -export function createSCard(key: string): command_request.Command { +export function createSCard(key: GlideString): command_request.Command { return createCommand(RequestType.SCard, [key]); } @@ -1142,8 +1142,8 @@ export function createSUnionStore( * @internal */ export function createSIsMember( - key: string, - member: string, + key: GlideString, + member: GlideString, ): command_request.Command { return createCommand(RequestType.SIsMember, [key, member]); } @@ -1152,8 +1152,8 @@ export function createSIsMember( * @internal */ export function createSMIsMember( - key: string, - members: string[], + key: GlideString, + members: GlideString[], ): command_request.Command { return createCommand(RequestType.SMIsMember, [key].concat(members)); } @@ -1174,10 +1174,11 @@ export function createSPop( * @internal */ export function createSRandMember( - key: string, + key: GlideString, count?: number, ): command_request.Command { - const args: string[] = count == undefined ? [key] : [key, count.toString()]; + const args: GlideString[] = + count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.SRandMember, args); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c75793df36..552ffb04e9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1348,7 +1348,11 @@ export class BaseTransaction> { * * Command Response - `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set. */ - public smove(source: string, destination: string, member: string): T { + public smove( + source: GlideString, + destination: GlideString, + member: GlideString, + ): T { return this.addAndReturn(createSMove(source, destination, member)); } @@ -1359,7 +1363,7 @@ export class BaseTransaction> { * * Command Response - the cardinality (number of elements) of the set, or 0 if key does not exist. */ - public scard(key: string): T { + public scard(key: GlideString): T { return this.addAndReturn(createSCard(key)); } @@ -1470,7 +1474,7 @@ export class BaseTransaction> { * Command Response - `true` if the member exists in the set, `false` otherwise. * If `key` doesn't exist, it is treated as an empty set and the command returns `false`. */ - public sismember(key: string, member: string): T { + public sismember(key: GlideString, member: GlideString): T { return this.addAndReturn(createSIsMember(key, member)); } @@ -1485,7 +1489,7 @@ export class BaseTransaction> { * * Command Response - An `array` of `boolean` values, each indicating if the respective member exists in the set. */ - public smismember(key: string, members: string[]): T { + public smismember(key: GlideString, members: GlideString[]): T { return this.addAndReturn(createSMIsMember(key, members)); } @@ -1522,7 +1526,7 @@ export class BaseTransaction> { * @param key - The key from which to retrieve the set member. * Command Response - A random element from the set, or null if `key` does not exist. */ - public srandmember(key: string): T { + public srandmember(key: GlideString): T { return this.addAndReturn(createSRandMember(key)); } @@ -1536,7 +1540,7 @@ export class BaseTransaction> { * If `count` is negative, allows for duplicates members. * Command Response - A list of members from the set. If the set does not exist or is empty, an empty list will be returned. */ - public srandmemberCount(key: string, count: number): T { + public srandmemberCount(key: GlideString, count: number): T { return this.addAndReturn(createSRandMember(key, count)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index cd3ae649e1..29c43305a1 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2725,6 +2725,7 @@ export function runBaseTests(config: { ).toEqual( new Set([Buffer.from("member4"), Buffer.from("member5")]), ); + expect(await client.scard(Buffer.from(key))).toEqual(2); }, protocol); }, config.timeout, @@ -2743,8 +2744,8 @@ export function runBaseTests(config: { expect(await client.sadd(key1, ["1", "2", "3"])).toEqual(3); expect(await client.sadd(key2, ["2", "3"])).toEqual(2); - // move an element - expect(await client.smove(key1, key2, "1")); + // move an element, test key as buffer + expect(await client.smove(Buffer.from(key1), key2, "1")); expect(await client.smembers(key1)).toEqual( new Set(["2", "3"]), ); @@ -2752,8 +2753,8 @@ export function runBaseTests(config: { new Set(["1", "2", "3"]), ); - // moved element already exists in the destination set - expect(await client.smove(key2, key1, "2")); + // moved element already exists in the destination set, test member as buffer + expect(await client.smove(key2, key1, Buffer.from("2"))); expect(await client.smembers(key1)).toEqual( new Set(["2", "3"]), ); @@ -3379,6 +3380,12 @@ export function runBaseTests(config: { const key2 = uuidv4(); expect(await client.sadd(key1, ["member1"])).toEqual(1); expect(await client.sismember(key1, "member1")).toEqual(true); + expect( + await client.sismember( + Buffer.from(key1), + Buffer.from("member1"), + ), + ).toEqual(true); expect( await client.sismember(key1, "nonExistingMember"), ).toEqual(false); @@ -3408,10 +3415,12 @@ export function runBaseTests(config: { 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(Buffer.from(key), [ + Buffer.from("b"), + "c", + ]), + ).toEqual([true, false]); expect(await client.smismember(nonExistingKey, ["b"])).toEqual([ false, @@ -3490,11 +3499,25 @@ export function runBaseTests(config: { null, ); + // with key and return value as buffers + const result3 = await client.srandmember(Buffer.from(key), { + decoder: Decoder.Bytes, + }); + expect(members).toContain(result3?.toString()); + // unique values are expected as count is positive let result = await client.srandmemberCount(key, 4); expect(result.length).toEqual(3); expect(new Set(result)).toEqual(new Set(members)); + // with key and return value as buffers + result = await client.srandmemberCount(Buffer.from(key), 4, { + decoder: Decoder.Bytes, + }); + expect(new Set(result)).toEqual( + new Set(members.map((member) => Buffer.from(member))), + ); + // duplicate values are expected as count is negative result = await client.srandmemberCount(key, -4); expect(result.length).toEqual(4); From 84d9a2920f7ee6c5a95a2b9da53fc0b5948924f5 Mon Sep 17 00:00:00 2001 From: liorsventitzky Date: Tue, 27 Aug 2024 17:27:34 +0300 Subject: [PATCH 228/236] Node: Add binary option to sscan strlen sunion sunionstore (#2199) * add binary option to sscan strlen sunion sunionstore Signed-off-by: lior sventitzky --- node/src/BaseClient.ts | 33 ++++++++++++++--------- node/src/Commands.ts | 14 +++++----- node/src/Transaction.ts | 12 ++++++--- node/tests/SharedTests.ts | 55 ++++++++++++++++++++++++++++++++------- 4 files changed, 80 insertions(+), 34 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1774f3eb98..bda29ae255 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2578,7 +2578,7 @@ export class BaseClient { * * @param key - The key of the set. * @param cursor - The cursor that points to the next iteration of results. A value of `"0"` indicates the start of the search. - * @param options - The (Optional) {@link BaseScanOptions}. + * @param options - (Optional) See {@link BaseScanOptions} and {@link DecoderOption}. * @returns An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and for the next iteration of results. * The `cursor` will be `"0"` on the last iteration of the set. The second element is always an array of the subset of the set held in `key`. * @@ -2608,11 +2608,14 @@ export class BaseClient { * ``` */ public async sscan( - key: string, - cursor: string, - options?: BaseScanOptions, - ): Promise<[string, string[]]> { - return this.createWritePromise(createSScan(key, cursor, options)); + key: GlideString, + cursor: GlideString, + options?: BaseScanOptions & DecoderOption, + ): Promise<[GlideString, GlideString[]]> { + return this.createWritePromise( + createSScan(key, cursor, options), + options, + ); } /** Returns all the members of the set value stored at `key`. @@ -2833,6 +2836,7 @@ export class BaseClient { * @remarks When in cluster mode, all `keys` must map to the same hash slot. * * @param keys - The keys of the sets. + * @param options - (Optional) See {@link DecoderOption}. * @returns A `Set` of members which are present in at least one of the given sets. * If none of the sets exist, an empty `Set` will be returned. * @@ -2847,10 +2851,13 @@ export class BaseClient { * console.log(result2); // Output: Set {'member1', 'member2'} * ``` */ - public async sunion(keys: string[]): Promise> { - return this.createWritePromise(createSUnion(keys)).then( - (sunion) => new Set(sunion), - ); + public async sunion( + keys: GlideString[], + options?: DecoderOption, + ): Promise> { + return this.createWritePromise(createSUnion(keys), { + decoder: options?.decoder, + }).then((sunion) => new Set(sunion)); } /** @@ -2871,8 +2878,8 @@ export class BaseClient { * ``` */ public async sunionstore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): Promise { return this.createWritePromise(createSUnionStore(destination, keys)); } @@ -4217,7 +4224,7 @@ export class BaseClient { * console.log(len2); // Output: 0 * ``` */ - public async strlen(key: string): Promise { + public async strlen(key: GlideString): Promise { return this.createWritePromise(createStrlen(key)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 14df8b48cc..171b065138 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1031,11 +1031,11 @@ export function createSRem( * @internal */ export function createSScan( - key: string, - cursor: string, + key: GlideString, + cursor: GlideString, options?: BaseScanOptions, ): command_request.Command { - let args: string[] = [key, cursor]; + let args: GlideString[] = [key, cursor]; if (options) { args = args.concat(convertBaseScanOptionsToArgsArray(options)); @@ -1124,7 +1124,7 @@ export function createSDiffStore( /** * @internal */ -export function createSUnion(keys: string[]): command_request.Command { +export function createSUnion(keys: GlideString[]): command_request.Command { return createCommand(RequestType.SUnion, keys); } @@ -1132,8 +1132,8 @@ export function createSUnion(keys: string[]): command_request.Command { * @internal */ export function createSUnionStore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): command_request.Command { return createCommand(RequestType.SUnionStore, [destination].concat(keys)); } @@ -1852,7 +1852,7 @@ export function createType(key: GlideString): command_request.Command { /** * @internal */ -export function createStrlen(key: string): command_request.Command { +export function createStrlen(key: GlideString): command_request.Command { return createCommand(RequestType.Strlen, [key]); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 552ffb04e9..05f0d229b3 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1322,7 +1322,11 @@ export class BaseTransaction> { * Command Response - An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and for the next iteration of results. * The `cursor` will be `"0"` on the last iteration of the set. The second element is always an array of the subset of the set held in `key`. */ - public sscan(key: string, cursor: string, options?: BaseScanOptions): T { + public sscan( + key: GlideString, + cursor: GlideString, + options?: BaseScanOptions, + ): T { return this.addAndReturn(createSScan(key, cursor, options)); } @@ -1446,7 +1450,7 @@ export class BaseTransaction> { * Command Response - A `Set` of members which are present in at least one of the given sets. * If none of the sets exist, an empty `Set` will be returned. */ - public sunion(keys: string[]): T { + public sunion(keys: GlideString[]): T { return this.addAndReturn(createSUnion(keys), true); } @@ -1461,7 +1465,7 @@ export class BaseTransaction> { * * Command Response - The number of elements in the resulting set. */ - public sunionstore(destination: string, keys: string[]): T { + public sunionstore(destination: GlideString, keys: GlideString[]): T { return this.addAndReturn(createSUnionStore(destination, keys)); } @@ -2171,7 +2175,7 @@ export class BaseTransaction> { * Command Response - The length of the string value stored at `key` * If `key` does not exist, it is treated as an empty string, and the command returns 0. */ - public strlen(key: string): T { + public strlen(key: GlideString): T { return this.addAndReturn(createStrlen(key)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 29c43305a1..df350b6344 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -30,6 +30,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + GlideString, InfBoundary, InfoOptions, InsertPosition, @@ -1550,9 +1551,9 @@ export function runBaseTests(config: { expect(result[resultCursorIndex]).toEqual(initialCursor); expect(result[resultCollectionIndex]).toEqual([]); - result = await client.sscan(key1, initialCursor); - expect(result[resultCursorIndex]).toEqual(initialCursor); - expect(result[resultCollectionIndex]).toEqual([]); + let result2 = await client.sscan(key1, initialCursor); + expect(result2[resultCursorIndex]).toEqual(initialCursor); + expect(result2[resultCollectionIndex]).toEqual([]); // Negative cursor if (cluster.checkIfServerVersionLessThan("7.9.0")) { @@ -1560,9 +1561,9 @@ export function runBaseTests(config: { expect(result[resultCursorIndex]).toEqual(initialCursor); expect(result[resultCollectionIndex]).toEqual([]); - result = await client.sscan(key1, "-1"); - expect(result[resultCursorIndex]).toEqual(initialCursor); - expect(result[resultCollectionIndex]).toEqual([]); + result2 = await client.sscan(key1, "-1"); + expect(result2[resultCursorIndex]).toEqual(initialCursor); + expect(result2[resultCollectionIndex]).toEqual([]); } else { await expect(client.hscan(key1, "-1")).rejects.toThrow( RequestError, @@ -3203,6 +3204,20 @@ export function runBaseTests(config: { ); expect(allResultMember).toEqual(true); + // Test with key, cursor, result value as binary buffers + const encodedResult = await client.sscan( + Buffer.from(key1), + Buffer.from(initialCursor), + { decoder: Decoder.Bytes }, + ); + const encodedResultMembers = encodedResult[ + resultCollectionIndex + ] as GlideString[]; + const allEncodedResultMembers = encodedResultMembers.every( + (member) => charMembersSet.has(member.toString()), + ); + expect(allEncodedResultMembers).toEqual(true); + // Testing sscan with match result = await client.sscan(key1, initialCursor, { match: "a", @@ -3216,7 +3231,7 @@ export function runBaseTests(config: { ); let resultCursor = "0"; - let secondResultValues: string[] = []; + let secondResultValues: GlideString[] = []; let isFirstLoop = true; @@ -3296,6 +3311,19 @@ export function runBaseTests(config: { new Set(["a", "b", "c", "d", "e"]), ); + // with return value as binary buffers + expect( + await client.sunion([key1, Buffer.from(key2)], { + decoder: Decoder.Bytes, + }), + ).toEqual( + new Set( + ["a", "b", "c", "d", "e"].map((member) => + Buffer.from(member), + ), + ), + ); + // invalid argument - key list must not be empty await expect(client.sunion([])).rejects.toThrow(); @@ -3333,8 +3361,13 @@ export function runBaseTests(config: { new Set(["a", "b", "c", "d", "e"]), ); - // overwrite existing set - expect(await client.sunionstore(key1, [key4, key2])).toEqual(5); + // overwrite existing set, test with binary option + expect( + await client.sunionstore(Buffer.from(key1), [ + Buffer.from(key4), + Buffer.from(key2), + ]), + ).toEqual(5); expect(await client.smembers(key1)).toEqual( new Set(["a", "b", "c", "d", "e"]), ); @@ -5672,7 +5705,9 @@ export function runBaseTests(config: { expect(await client.set(key1, key1Value)).toEqual("OK"); expect(await client.strlen(key1)).toEqual(key1ValueLength); - expect(await client.strlen("nonExistKey")).toEqual(0); + expect(await client.strlen(Buffer.from("nonExistKey"))).toEqual( + 0, + ); const listName = "myList"; const listKey1Value = uuidv4(); From 70b0ae845303ced1daf230068ba051c422a4b018 Mon Sep 17 00:00:00 2001 From: eifrah-aws Date: Wed, 28 Aug 2024 17:55:47 +0300 Subject: [PATCH 229/236] GO: performance improvement (#2196) * Performance improvement for GO client - Use the go string internal pointer in Rust code - Do not copy go string pointers when building Redis command in lib.rs - Disabled go1.18 Signed-off-by: Eran Ifrah --- .github/workflows/go.yml | 2 -- go/Makefile | 17 +++++++++++++++- go/api/base_client.go | 24 ++++++++++++---------- go/go.mod | 2 +- go/src/lib.rs | 44 +++++++++++++++++++++++++--------------- 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7f6bd60a4a..7cdfedef59 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,7 +52,6 @@ jobs: fail-fast: false matrix: go: - # - '1.18.10' - '1.22.0' engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} host: @@ -130,7 +129,6 @@ jobs: fail-fast: false matrix: go: - - 1.18.10 - 1.22.0 runs-on: ubuntu-latest container: amazonlinux:latest diff --git a/go/Makefile b/go/Makefile index f5bfb7a0ca..b15d198771 100644 --- a/go/Makefile +++ b/go/Makefile @@ -28,11 +28,26 @@ install-tools: install-tools-go1.22.0 build: build-glide-client generate-protobuf go build ./... + cd benchmarks && go build -ldflags="-w" ./... + +build-debug: build-glide-client-debug generate-protobuf + go build -gcflags "-l -N" ./... + cd benchmarks && go build -gcflags "-l -N" ./... + +clean: + go clean + rm -f lib.h + rm -f benchmarks/benchmarks + build-glide-client: cargo build --release cbindgen --config cbindgen.toml --crate glide-rs --output lib.h +build-glide-client-debug: + cargo build + cbindgen --config cbindgen.toml --crate glide-rs --output lib.h + generate-protobuf: mkdir -p protobuf protoc --proto_path=../glide-core/src/protobuf \ @@ -58,7 +73,7 @@ format: golines -w --shorten-comments -m 127 . test: - LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ + LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|grep -w release|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ go test -v -race ./... # Note: this task is no longer run by CI because: diff --git a/go/api/base_client.go b/go/api/base_client.go index 48da19cb8f..50938612aa 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -100,7 +100,6 @@ func (client *baseClient) executeCommand(requestType C.RequestType, args []strin } cArgs, argLengths := toCStrings(args) - defer freeCStrings(cArgs) resultChannel := make(chan payload) resultChannelPtr := uintptr(unsafe.Pointer(&resultChannel)) @@ -120,23 +119,26 @@ func (client *baseClient) executeCommand(requestType C.RequestType, args []strin return payload.value, nil } -// TODO: Handle passing the arguments as strings without assuming null termination assumption. -func toCStrings(args []string) ([]*C.char, []C.ulong) { - cStrings := make([]*C.char, len(args)) +// Convert `s` of type `string` into `[]byte` +func StringToBytes(s string) []byte { + p := unsafe.StringData(s) + b := unsafe.Slice(p, len(s)) + return b +} + +// Zero copying conversion from go's []string into C pointers +func toCStrings(args []string) ([]C.uintptr_t, []C.ulong) { + cStrings := make([]C.uintptr_t, len(args)) stringLengths := make([]C.ulong, len(args)) for i, str := range args { - cStrings[i] = C.CString(str) + bytes := StringToBytes(str) + ptr := uintptr(unsafe.Pointer(&bytes[0])) + cStrings[i] = C.uintptr_t(ptr) stringLengths[i] = C.size_t(len(str)) } return cStrings, stringLengths } -func freeCStrings(cArgs []*C.char) { - for _, arg := range cArgs { - C.free(unsafe.Pointer(arg)) - } -} - func (client *baseClient) Set(key string, value string) (string, error) { result, err := client.executeCommand(C.Set, []string{key, value}) if err != nil { diff --git a/go/go.mod b/go/go.mod index 8ce0f5f6fd..cbca0b10fa 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,6 +1,6 @@ module github.com/valkey-io/valkey-glide/go/glide -go 1.18 +go 1.20 require ( github.com/stretchr/testify v1.8.4 diff --git a/go/src/lib.rs b/go/src/lib.rs index a1cf07dda8..997b9c9d88 100644 --- a/go/src/lib.rs +++ b/go/src/lib.rs @@ -250,7 +250,8 @@ pub unsafe extern "C" fn free_command_response(command_response_ptr: *mut Comman /// /// # Safety /// -/// `free_error_message` can only be called once per `error_message`. Calling it twice is undefined behavior, since the address will be freed twice. +/// `free_error_message` can only be called once per `error_message`. Calling it twice is undefined +/// behavior, since the address will be freed twice. #[no_mangle] pub unsafe extern "C" fn free_error_message(error_message: *mut c_char) { assert!(!error_message.is_null()); @@ -258,17 +259,23 @@ pub unsafe extern "C" fn free_error_message(error_message: *mut c_char) { } /// Converts a double pointer to a vec. -unsafe fn convert_double_pointer_to_vec( - data: *const *const c_char, +/// +/// # Safety +/// +/// `convert_double_pointer_to_vec` returns a `Vec` of u8 slice which holds pointers of `go` +/// strings. The returned `Vec<&'a [u8]>` is meant to be copied into Rust code. Storing them +/// for later use will cause the program to crash as the pointers will be freed by go's gc +unsafe fn convert_double_pointer_to_vec<'a>( + data: *const *const c_void, len: c_ulong, data_len: *const c_ulong, -) -> Vec> { +) -> Vec<&'a [u8]> { let string_ptrs = unsafe { from_raw_parts(data, len as usize) }; let string_lengths = unsafe { from_raw_parts(data_len, len as usize) }; - let mut result: Vec> = Vec::new(); + let mut result = Vec::<&[u8]>::with_capacity(string_ptrs.len()); for (i, &str_ptr) in string_ptrs.iter().enumerate() { let slice = unsafe { from_raw_parts(str_ptr as *const u8, string_lengths[i] as usize) }; - result.push(slice.to_vec()); + result.push(slice); } result } @@ -293,25 +300,30 @@ pub unsafe extern "C" fn command( channel: usize, command_type: RequestType, arg_count: c_ulong, - args: *const *const c_char, + args: *const usize, args_len: *const c_ulong, ) { let client_adapter = unsafe { Box::leak(Box::from_raw(client_adapter_ptr as *mut ClientAdapter)) }; - // The safety of this needs to be ensured by the calling code. Cannot dispose of the pointer before all operations have completed. + // The safety of this needs to be ensured by the calling code. Cannot dispose of the pointer before + // all operations have completed. let ptr_address = client_adapter_ptr as usize; - let arg_vec = unsafe { convert_double_pointer_to_vec(args, arg_count, args_len) }; + let arg_vec = + unsafe { convert_double_pointer_to_vec(args as *const *const c_void, arg_count, args_len) }; let mut client_clone = client_adapter.client.clone(); - client_adapter.runtime.spawn(async move { - let mut cmd = command_type - .get_command() - .expect("Couldn't fetch command type"); - for slice in arg_vec { - cmd.arg(slice); - } + // Create the command outside of the task to ensure that the command arguments passed + // from "go" are still valid + let mut cmd = command_type + .get_command() + .expect("Couldn't fetch command type"); + for command_arg in arg_vec { + cmd.arg(command_arg); + } + + client_adapter.runtime.spawn(async move { let result = client_clone.send_command(&cmd, None).await; let client_adapter = unsafe { Box::leak(Box::from_raw(ptr_address as *mut ClientAdapter)) }; let value = match result { From 8abbfb3ca1eb7071ce69109ef9a675d79f3f67f6 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:43:19 -0700 Subject: [PATCH 230/236] Node: Add binary support for hash commands (#2194) * Update CHANGELOG Signed-off-by: Jonathan Louie * Add binary variants for hash commands Signed-off-by: Jonathan Louie * Apply Prettier Signed-off-by: Jonathan Louie * Change args for hscan to be GlideString[] Signed-off-by: Jonathan Louie * Use GlideString for BaseScanOptions Signed-off-by: Jonathan Louie * Run Prettier Signed-off-by: Jonathan Louie * Exclude HSCAN command from changes for now Signed-off-by: Jonathan Louie * Revert change to return type of convertBaseScanOptionsToArgsArray Signed-off-by: Jonathan Louie * Revert accidental change of count to string for BaseScanOptions Signed-off-by: Jonathan Louie * Apply Prettier Signed-off-by: Jonathan Louie * Start changing hash commands to use DecoderOption Signed-off-by: Jonathan Louie * Add DecoderOption for hash commands Signed-off-by: Jonathan Louie * Update SharedTests Signed-off-by: Jonathan Louie * Shorten DecoderOption parameter descriptions Signed-off-by: Jonathan Louie * Apply Prettier Signed-off-by: Jonathan Louie * Try to fix test failures Signed-off-by: Jonathan Louie * Switch to toContainEqual for hrandfield binary test Signed-off-by: Jonathan Louie --------- Signed-off-by: Jonathan Louie Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 83 ++++++++++++++++++++++------------ node/src/Commands.ts | 40 ++++++++--------- node/src/Transaction.ts | 32 +++++++------ node/tests/SharedTests.ts | 95 +++++++++++++++++++++++++++++---------- 5 files changed, 165 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7361c001b2..b7639dba5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to HASH commands ([#2194](https://github.com/valkey-io/valkey-glide/pull/2194)) * Node: Added binary variant to server management commands ([#2179](https://github.com/valkey-io/valkey-glide/pull/2179)) * Node: Added/updated binary variant to connection management commands and WATCH/UNWATCH ([#2160](https://github.com/valkey-io/valkey-glide/pull/2160)) * Java: Fix docs for stream commands ([#2086](https://github.com/valkey-io/valkey-glide/pull/2086)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index bda29ae255..8079df1e07 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1651,7 +1651,7 @@ export class BaseClient { * ``` */ public async hset( - key: string, + key: GlideString, fieldValueMap: Record, ): Promise { return this.createWritePromise(createHSet(key, fieldValueMap)); @@ -1663,6 +1663,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/hkeys/|valkey.io} for details. * * @param key - The key of the hash. + * @param options - (Optional) See {@link DecoderOption}. * @returns A list of field names for the hash, or an empty list when the key does not exist. * * @example @@ -1673,8 +1674,12 @@ export class BaseClient { * console.log(result); // Output: ["field1", "field2", "field3"] - Returns all the field names stored in the hash "my_hash". * ``` */ - public async hkeys(key: string): Promise { - return this.createWritePromise(createHKeys(key)); + + public async hkeys( + key: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createHKeys(key), options); } /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. @@ -1703,9 +1708,9 @@ export class BaseClient { * ``` */ public async hsetnx( - key: string, - field: string, - value: string, + key: GlideString, + field: GlideString, + value: GlideString, ): Promise { return this.createWritePromise(createHSetNX(key, field, value)); } @@ -1727,7 +1732,10 @@ export class BaseClient { * console.log(result); // Output: 2 - Indicates that two fields were successfully removed from the hash. * ``` */ - public async hdel(key: string, fields: string[]): Promise { + public async hdel( + key: GlideString, + fields: GlideString[], + ): Promise { return this.createWritePromise(createHDel(key, fields)); } @@ -1737,6 +1745,7 @@ export class BaseClient { * * @param key - The key of the hash. * @param fields - The fields in the hash stored at `key` to retrieve from the database. + * @param options - (Optional) See {@link DecoderOption}. * @returns a list of values associated with the given fields, in the same order as they are requested. * For every field that does not exist in the hash, a null value is returned. * If `key` does not exist, it is treated as an empty hash and it returns a list of null values. @@ -1749,10 +1758,11 @@ export class BaseClient { * ``` */ public async hmget( - key: string, - fields: string[], - ): Promise<(string | null)[]> { - return this.createWritePromise(createHMGet(key, fields)); + key: GlideString, + fields: GlideString[], + options?: DecoderOption, + ): Promise<(GlideString | null)[]> { + return this.createWritePromise(createHMGet(key, fields), options); } /** Returns if `field` is an existing field in the hash stored at `key`. @@ -1777,7 +1787,10 @@ export class BaseClient { * console.log(result); // Output: false * ``` */ - public async hexists(key: string, field: string): Promise { + public async hexists( + key: GlideString, + field: GlideString, + ): Promise { return this.createWritePromise(createHExists(key, field)); } @@ -1796,7 +1809,7 @@ export class BaseClient { * console.log(result); // Output: {"field1": "value1", "field2": "value2"} * ``` */ - public async hgetall(key: string): Promise> { + public async hgetall(key: GlideString): Promise> { return this.createWritePromise(createHGetAll(key)); } @@ -1819,8 +1832,8 @@ export class BaseClient { * ``` */ public async hincrBy( - key: string, - field: string, + key: GlideString, + field: GlideString, amount: number, ): Promise { return this.createWritePromise(createHIncrBy(key, field, amount)); @@ -1841,12 +1854,12 @@ export class BaseClient { * ```typescript * // Example usage of the hincrbyfloat method to increment the value of a floating point in a hash by a specified amount * const result = await client.hincrbyfloat("my_hash", "field1", 2.5); - * console.log(result); // Output: '2.5' + * console.log(result); // Output: 2.5 * ``` */ public async hincrByFloat( - key: string, - field: string, + key: GlideString, + field: GlideString, amount: number, ): Promise { return this.createWritePromise(createHIncrByFloat(key, field, amount)); @@ -1873,7 +1886,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public async hlen(key: string): Promise { + public async hlen(key: GlideString): Promise { return this.createWritePromise(createHLen(key)); } @@ -1916,7 +1929,10 @@ export class BaseClient { * console.log(result); // Output: 5 * ``` */ - public async hstrlen(key: string, field: string): Promise { + public async hstrlen( + key: GlideString, + field: GlideString, + ): Promise { return this.createWritePromise(createHStrlen(key, field)); } @@ -1927,6 +1943,7 @@ export class BaseClient { * @remarks Since Valkey version 6.2.0. * * @param key - The key of the hash. + * @param options - (Optional) See {@link DecoderOption}. * @returns A random field name from the hash stored at `key`, or `null` when * the key does not exist. * @@ -1935,8 +1952,11 @@ export class BaseClient { * console.log(await client.hrandfield("myHash")); // Output: 'field' * ``` */ - public async hrandfield(key: string): Promise { - return this.createWritePromise(createHRandField(key)); + public async hrandfield( + key: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createHRandField(key), options); } /** @@ -1992,6 +2012,7 @@ export class BaseClient { * * @param key - The key of the hash. * @param count - The number of field names to return. + * @param options - (Optional) See {@link DecoderOption}. * * If `count` is positive, returns unique elements. If negative, allows for duplicates. * @returns An `array` of random field names from the hash stored at `key`, @@ -2003,10 +2024,11 @@ export class BaseClient { * ``` */ public async hrandfieldCount( - key: string, + key: GlideString, count: number, - ): Promise { - return this.createWritePromise(createHRandField(key, count)); + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createHRandField(key, count), options); } /** @@ -2018,6 +2040,7 @@ export class BaseClient { * * @param key - The key of the hash. * @param count - The number of field names to return. + * @param options - (Optional) See {@link DecoderOption}. * * If `count` is positive, returns unique elements. If negative, allows for duplicates. * @returns A 2D `array` of `[fieldName, value]` `arrays`, where `fieldName` is a random @@ -2031,10 +2054,14 @@ export class BaseClient { * ``` */ public async hrandfieldWithValues( - key: string, + key: GlideString, count: number, - ): Promise<[string, string][]> { - return this.createWritePromise(createHRandField(key, count, true)); + options?: DecoderOption, + ): Promise<[GlideString, GlideString][]> { + return this.createWritePromise( + createHRandField(key, count, true), + options, + ); } /** Inserts all the specified values at the head of the list stored at `key`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 171b065138..22dd1f95ba 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -412,7 +412,7 @@ export function createHGet( * @internal */ export function createHSet( - key: string, + key: GlideString, fieldValueMap: Record, ): command_request.Command { return createCommand( @@ -424,7 +424,7 @@ export function createHSet( /** * @internal */ -export function createHKeys(key: string): command_request.Command { +export function createHKeys(key: GlideString): command_request.Command { return createCommand(RequestType.HKeys, [key]); } @@ -432,9 +432,9 @@ export function createHKeys(key: string): command_request.Command { * @internal */ export function createHSetNX( - key: string, - field: string, - value: string, + key: GlideString, + field: GlideString, + value: GlideString, ): command_request.Command { return createCommand(RequestType.HSetNX, [key, field, value]); } @@ -801,8 +801,8 @@ export function createBitField( * @internal */ export function createHDel( - key: string, - fields: string[], + key: GlideString, + fields: GlideString[], ): command_request.Command { return createCommand(RequestType.HDel, [key].concat(fields)); } @@ -811,8 +811,8 @@ export function createHDel( * @internal */ export function createHMGet( - key: string, - fields: string[], + key: GlideString, + fields: GlideString[], ): command_request.Command { return createCommand(RequestType.HMGet, [key].concat(fields)); } @@ -821,8 +821,8 @@ export function createHMGet( * @internal */ export function createHExists( - key: string, - field: string, + key: GlideString, + field: GlideString, ): command_request.Command { return createCommand(RequestType.HExists, [key, field]); } @@ -830,7 +830,7 @@ export function createHExists( /** * @internal */ -export function createHGetAll(key: string): command_request.Command { +export function createHGetAll(key: GlideString): command_request.Command { return createCommand(RequestType.HGetAll, [key]); } @@ -1193,8 +1193,8 @@ export function createCustomCommand(args: GlideString[]) { * @internal */ export function createHIncrBy( - key: string, - field: string, + key: GlideString, + field: GlideString, amount: number, ): command_request.Command { return createCommand(RequestType.HIncrBy, [key, field, amount.toString()]); @@ -1204,8 +1204,8 @@ export function createHIncrBy( * @internal */ export function createHIncrByFloat( - key: string, - field: string, + key: GlideString, + field: GlideString, amount: number, ): command_request.Command { return createCommand(RequestType.HIncrByFloat, [ @@ -1218,7 +1218,7 @@ export function createHIncrByFloat( /** * @internal */ -export function createHLen(key: string): command_request.Command { +export function createHLen(key: GlideString): command_request.Command { return createCommand(RequestType.HLen, [key]); } @@ -3618,15 +3618,15 @@ function createSortImpl( * @internal */ export function createHStrlen( - key: string, - field: string, + key: GlideString, + field: GlideString, ): command_request.Command { return createCommand(RequestType.HStrlen, [key, field]); } /** @internal */ export function createHRandField( - key: string, + key: GlideString, count?: number, withValues?: boolean, ): command_request.Command { diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 05f0d229b3..3d9de03d50 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -809,7 +809,7 @@ export class BaseTransaction> { * * Command Response - The number of fields that were added. */ - public hset(key: string, fieldValueMap: Record): T { + public hset(key: GlideString, fieldValueMap: Record): T { return this.addAndReturn(createHSet(key, fieldValueMap)); } @@ -822,7 +822,7 @@ export class BaseTransaction> { * * Command Response - A list of field names for the hash, or an empty list when the key does not exist. */ - public hkeys(key: string): T { + public hkeys(key: GlideString): T { return this.addAndReturn(createHKeys(key)); } @@ -837,7 +837,7 @@ export class BaseTransaction> { * * Command Response - `true` if the field was set, `false` if the field already existed and was not set. */ - public hsetnx(key: string, field: string, value: string): T { + public hsetnx(key: GlideString, field: GlideString, value: GlideString): T { return this.addAndReturn(createHSetNX(key, field, value)); } @@ -851,7 +851,7 @@ export class BaseTransaction> { * Command Response - the number of fields that were removed from the hash, not including specified but non existing fields. * If `key` does not exist, it is treated as an empty hash and it returns 0. */ - public hdel(key: string, fields: string[]): T { + public hdel(key: GlideString, fields: GlideString[]): T { return this.addAndReturn(createHDel(key, fields)); } @@ -865,7 +865,7 @@ export class BaseTransaction> { * For every field that does not exist in the hash, a null value is returned. * If `key` does not exist, it is treated as an empty hash and it returns a list of null values. */ - public hmget(key: string, fields: string[]): T { + public hmget(key: GlideString, fields: GlideString[]): T { return this.addAndReturn(createHMGet(key, fields)); } @@ -878,7 +878,7 @@ export class BaseTransaction> { * Command Response - `true` if the hash contains `field`. If the hash does not contain `field`, or if `key` does not exist, * the command response will be `false`. */ - public hexists(key: string, field: string): T { + public hexists(key: GlideString, field: GlideString): T { return this.addAndReturn(createHExists(key, field)); } @@ -890,7 +890,7 @@ export class BaseTransaction> { * Command Response - a map of fields and their values stored in the hash. Every field name in the map is followed by its value. * If `key` does not exist, it returns an empty map. */ - public hgetall(key: string): T { + public hgetall(key: GlideString): T { return this.addAndReturn(createHGetAll(key)); } @@ -905,7 +905,7 @@ export class BaseTransaction> { * * Command Response - the value of `field` in the hash stored at `key` after the increment. */ - public hincrBy(key: string, field: string, amount: number): T { + public hincrBy(key: GlideString, field: GlideString, amount: number): T { return this.addAndReturn(createHIncrBy(key, field, amount)); } @@ -920,7 +920,11 @@ export class BaseTransaction> { * * Command Response - the value of `field` in the hash stored at `key` after the increment. */ - public hincrByFloat(key: string, field: string, amount: number): T { + public hincrByFloat( + key: GlideString, + field: GlideString, + amount: number, + ): T { return this.addAndReturn(createHIncrByFloat(key, field, amount)); } @@ -931,7 +935,7 @@ export class BaseTransaction> { * * Command Response - The number of fields in the hash, or 0 when the key does not exist. */ - public hlen(key: string): T { + public hlen(key: GlideString): T { return this.addAndReturn(createHLen(key)); } @@ -956,7 +960,7 @@ export class BaseTransaction> { * * Command Response - The string length or `0` if `field` or `key` does not exist. */ - public hstrlen(key: string, field: string): T { + public hstrlen(key: GlideString, field: GlideString): T { return this.addAndReturn(createHStrlen(key, field)); } @@ -971,7 +975,7 @@ export class BaseTransaction> { * Command Response - A random field name from the hash stored at `key`, or `null` when * the key does not exist. */ - public hrandfield(key: string): T { + public hrandfield(key: GlideString): T { return this.addAndReturn(createHRandField(key)); } @@ -1008,7 +1012,7 @@ export class BaseTransaction> { * Command Response - An `array` of random field names from the hash stored at `key`, * or an `empty array` when the key does not exist. */ - public hrandfieldCount(key: string, count: number): T { + public hrandfieldCount(key: GlideString, count: number): T { return this.addAndReturn(createHRandField(key, count)); } @@ -1028,7 +1032,7 @@ export class BaseTransaction> { * field name from the hash and `value` is the associated value of the field name. * If the hash does not exist or is empty, the response will be an empty `array`. */ - public hrandfieldWithValues(key: string, count: number): T { + public hrandfieldWithValues(key: GlideString, count: number): T { return this.addAndReturn(createHRandField(key, count, true)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index df350b6344..c3b82e046b 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1332,7 +1332,9 @@ export function runBaseTests(config: { const valueEncoded = Buffer.from(value); expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(await client.hget(key, field1)).toEqual(value); + expect( + await client.hget(Buffer.from(key), Buffer.from(field1)), + ).toEqual(value); expect(await client.hget(key, field2)).toEqual(value); expect(await client.hget(key, "nonExistingField")).toEqual( null, @@ -1367,6 +1369,7 @@ export function runBaseTests(config: { [field1]: value, [field2]: value2, }; + const field2Encoded = Buffer.from(field2); // set up hash with two keys/values expect(await client.hset(key, fieldValueMap)).toEqual(2); @@ -1374,7 +1377,11 @@ export function runBaseTests(config: { // remove one key expect(await client.hdel(key, [field1])).toEqual(1); - expect(await client.hkeys(key)).toEqual([field2]); + expect( + await client.hkeys(Buffer.from(key), { + decoder: Decoder.Bytes, + }), + ).toEqual([field2Encoded]); // non-existing key returns an empty list expect(await client.hkeys("nonExistingKey")).toEqual([]); @@ -1657,7 +1664,9 @@ export function runBaseTests(config: { }; expect(await client.hset(key, fieldValueMap)).toEqual(3); - expect(await client.hdel(key, [field1, field2])).toEqual(2); + expect( + await client.hdel(Buffer.from(key), [field1, field2]), + ).toEqual(2); expect(await client.hdel(key, ["nonExistingField"])).toEqual(0); expect(await client.hdel("nonExistingKey", [field3])).toEqual( 0, @@ -1688,7 +1697,11 @@ export function runBaseTests(config: { ]), ).toEqual([value, null, value]); expect( - await client.hmget("nonExistingKey", [field1, field2]), + await client.hmget( + Buffer.from("nonExistingKey"), + [field1, field2], + { decoder: Decoder.Bytes }, + ), ).toEqual([null, null]); }, protocol); }, @@ -1707,7 +1720,9 @@ export function runBaseTests(config: { [field2]: "value2", }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(await client.hexists(key, field1)).toEqual(true); + expect( + await client.hexists(Buffer.from(key), Buffer.from(field1)), + ).toEqual(true); expect(await client.hexists(key, "nonExistingField")).toEqual( false, ); @@ -1738,7 +1753,9 @@ export function runBaseTests(config: { [field2]: value, }); - expect(await client.hgetall("nonExistingKey")).toEqual({}); + expect( + await client.hgetall(Buffer.from("nonExistingKey")), + ).toEqual({}); }, protocol); }, config.timeout, @@ -1755,10 +1772,20 @@ export function runBaseTests(config: { }; expect(await client.hset(key, fieldValueMap)).toEqual(1); expect(await client.hincrBy(key, field, 1)).toEqual(11); - expect(await client.hincrBy(key, field, 4)).toEqual(15); - expect(await client.hincrByFloat(key, field, 1.5)).toEqual( - 16.5, - ); + expect( + await client.hincrBy( + Buffer.from(key), + Buffer.from(field), + 4, + ), + ).toEqual(15); + expect( + await client.hincrByFloat( + Buffer.from(key), + Buffer.from(field), + 1.5, + ), + ).toEqual(16.5); }, protocol); }, config.timeout, @@ -1838,7 +1865,7 @@ export function runBaseTests(config: { expect(await client.hset(key1, fieldValueMap)).toEqual(2); expect(await client.hlen(key1)).toEqual(2); expect(await client.hdel(key1, [field1])).toEqual(1); - expect(await client.hlen(key1)).toEqual(1); + expect(await client.hlen(Buffer.from(key1))).toEqual(1); expect(await client.hlen("nonExistingHash")).toEqual(0); }, protocol); }, @@ -1861,10 +1888,14 @@ export function runBaseTests(config: { const value1Encoded = Buffer.from("value1"); const value2Encoded = Buffer.from("value2"); - expect(await client.hset(key1, fieldValueMap)).toEqual(2); + expect( + await client.hset(Buffer.from(key1), fieldValueMap), + ).toEqual(2); expect(await client.hvals(key1)).toEqual(["value1", "value2"]); expect(await client.hdel(key1, [field1])).toEqual(1); - expect(await client.hvals(key1)).toEqual(["value2"]); + expect(await client.hvals(Buffer.from(key1))).toEqual([ + "value2", + ]); expect(await client.hvals("nonExistingHash")).toEqual([]); //hvals with binary buffers @@ -1891,9 +1922,13 @@ export function runBaseTests(config: { const field = uuidv4(); expect(await client.hsetnx(key1, field, "value")).toEqual(true); - expect(await client.hsetnx(key1, field, "newValue")).toEqual( - false, - ); + expect( + await client.hsetnx( + Buffer.from(key1), + Buffer.from(field), + "newValue", + ), + ).toEqual(false); expect(await client.hget(key1, field)).toEqual("value"); expect(await client.set(key2, "value")).toEqual("OK"); @@ -1924,9 +1959,9 @@ export function runBaseTests(config: { // key exists but holds non hash type value expect(await client.set(key2, "value")).toEqual("OK"); - await expect(client.hstrlen(key2, field)).rejects.toThrow( - RequestError, - ); + await expect( + client.hstrlen(Buffer.from(key2), Buffer.from(field)), + ).rejects.toThrow(RequestError); }, protocol); }, config.timeout, @@ -1944,13 +1979,19 @@ export function runBaseTests(config: { const key2 = uuidv4(); // key does not exist - expect(await client.hrandfield(key1)).toBeNull(); + expect( + await client.hrandfield(Buffer.from(key1), { + decoder: Decoder.Bytes, + }), + ).toBeNull(); expect(await client.hrandfieldCount(key1, 5)).toEqual([]); expect(await client.hrandfieldWithValues(key1, 5)).toEqual([]); const data = { "f 1": "v 1", "f 2": "v 2", "f 3": "v 3" }; const fields = Object.keys(data); const entries = Object.entries(data); + const encodedFields = fields.map(Buffer.from); + const encodedEntries = entries.map((e) => e.map(Buffer.from)); expect(await client.hset(key1, data)).toEqual(3); expect(fields).toContain(await client.hrandfield(key1)); @@ -1960,13 +2001,19 @@ export function runBaseTests(config: { expect(result).toEqual(fields); // With Count - negative count - result = await client.hrandfieldCount(key1, -5); + result = await client.hrandfieldCount(Buffer.from(key1), -5, { + decoder: Decoder.Bytes, + }); expect(result.length).toEqual(5); - result.map((r) => expect(fields).toContain(r)); + result.map((r) => expect(encodedFields).toContainEqual(r)); // With values - positive count - let result2 = await client.hrandfieldWithValues(key1, 5); - expect(result2).toEqual(entries); + let result2 = await client.hrandfieldWithValues( + Buffer.from(key1), + 5, + { decoder: Decoder.Bytes }, + ); + expect(result2).toEqual(encodedEntries); // With values - negative count result2 = await client.hrandfieldWithValues(key1, -5); From e48133f7e23f24dd4d16808ef6edb078dd4d3153 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Wed, 28 Aug 2024 11:30:11 -0700 Subject: [PATCH 231/236] Node: add binary support for PubSub commands, part 1 (#2201) * Node: add binary support for PubSub commands, part 1 --------- Signed-off-by: Yi-Pin Chen --- node/src/BaseClient.ts | 14 ++++++++++---- node/src/Commands.ts | 8 ++++---- node/src/GlideClient.ts | 5 ++++- node/src/GlideClusterClient.ts | 21 +++++++++++++++------ node/src/Transaction.ts | 10 +++++----- node/tests/PubSub.test.ts | 18 ++++++++++-------- 6 files changed, 48 insertions(+), 28 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8079df1e07..772e1e904b 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -6610,8 +6610,10 @@ export class BaseClient { * * @see {@link https://valkey.io/commands/pubsub-channels/|valkey.io} for more details. * - * @param pattern - A glob-style pattern to match active channels. - * If not provided, all active channels are returned. + * @param options - (Optional) Additional parameters: + * - (Optional) `pattern`: A glob-style pattern to match active channels. + * If not provided, all active channels are returned. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A list of currently active channels matching the given pattern. * If no pattern is specified, all active channels are returned. * @@ -6624,8 +6626,12 @@ export class BaseClient { * console.log(newsChannels); // Output: ["news.sports", "news.weather"] * ``` */ - public async pubsubChannels(pattern?: string): Promise { - return this.createWritePromise(createPubSubChannels(pattern)); + public async pubsubChannels( + options?: { pattern?: GlideString } & DecoderOption, + ): Promise { + return this.createWritePromise(createPubSubChannels(options?.pattern), { + decoder: options?.decoder, + }); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 22dd1f95ba..78fe767852 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2207,8 +2207,8 @@ export function createTime(): command_request.Command { * @internal */ export function createPublish( - message: string, - channel: string, + message: GlideString, + channel: GlideString, sharded: boolean = false, ): command_request.Command { const request = sharded ? RequestType.SPublish : RequestType.Publish; @@ -3855,7 +3855,7 @@ export function createBLMPop( * @internal */ export function createPubSubChannels( - pattern?: string, + pattern?: GlideString, ): command_request.Command { return createCommand(RequestType.PubSubChannels, pattern ? [pattern] : []); } @@ -3880,7 +3880,7 @@ export function createPubSubNumSub( * @internal */ export function createPubsubShardChannels( - pattern?: string, + pattern?: GlideString, ): command_request.Command { return createCommand(RequestType.PubSubSChannels, pattern ? [pattern] : []); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index ccebc647a7..3492fd715e 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -851,7 +851,10 @@ export class GlideClient extends BaseClient { * console.log(result); // Output: 1 - This message was posted to 1 subscription which is configured on primary node * ``` */ - public async publish(message: string, channel: string): Promise { + public async publish( + message: GlideString, + channel: GlideString, + ): Promise { return this.createWritePromise(createPublish(message, channel)); } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 967ea0e568..56e75aad16 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -1221,8 +1221,8 @@ export class GlideClusterClient extends BaseClient { * ``` */ public async publish( - message: string, - channel: string, + message: GlideString, + channel: GlideString, sharded: boolean = false, ): Promise { return this.createWritePromise( @@ -1236,8 +1236,10 @@ export class GlideClusterClient extends BaseClient { * * @see {@link https://valkey.io/commands/pubsub-shardchannels/|valkey.io} for details. * - * @param pattern - A glob-style pattern to match active shard channels. - * If not provided, all active shard channels are returned. + * @param options - (Optional) Additional parameters: + * - (Optional) `pattern`: A glob-style pattern to match active shard channels. + * If not provided, all active shard channels are returned. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A list of currently active shard channels matching the given pattern. * If no pattern is specified, all active shard channels are returned. * @@ -1250,8 +1252,15 @@ export class GlideClusterClient extends BaseClient { * console.log(filteredChannels); // Output: ["channel1", "channel2"] * ``` */ - public async pubsubShardChannels(pattern?: string): Promise { - return this.createWritePromise(createPubsubShardChannels(pattern)); + public async pubsubShardChannels( + options?: { + pattern?: GlideString; + } & DecoderOption, + ): Promise { + return this.createWritePromise( + createPubsubShardChannels(options?.pattern), + { decoder: options?.decoder }, + ); } /** diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 3d9de03d50..eabf799f1b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3841,7 +3841,7 @@ export class BaseTransaction> { * Command Response - A list of currently active channels matching the given pattern. * If no pattern is specified, all active channels are returned. */ - public pubsubChannels(pattern?: string): T { + public pubsubChannels(pattern?: GlideString): T { return this.addAndReturn(createPubSubChannels(pattern)); } @@ -4027,7 +4027,7 @@ export class Transaction extends BaseTransaction { * Command Response - Number of subscriptions in primary node that received the message. * Note that this value does not include subscriptions that configured on replicas. */ - public publish(message: string, channel: string): Transaction { + public publish(message: GlideString, channel: GlideString): Transaction { return this.addAndReturn(createPublish(message, channel)); } } @@ -4154,8 +4154,8 @@ export class ClusterTransaction extends BaseTransaction { * Command Response - Number of subscriptions in primary node that received the message. */ public publish( - message: string, - channel: string, + message: GlideString, + channel: GlideString, sharded: boolean = false, ): ClusterTransaction { return this.addAndReturn(createPublish(message, channel, sharded)); @@ -4172,7 +4172,7 @@ export class ClusterTransaction extends BaseTransaction { * Command Response - A list of currently active shard channels matching the given pattern. * If no pattern is specified, all active shard channels are returned. */ - public pubsubShardChannels(pattern?: string): ClusterTransaction { + public pubsubShardChannels(pattern?: GlideString): ClusterTransaction { return this.addAndReturn(createPubsubShardChannels(pattern)); } diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts index 76560635af..c065fa89ca 100644 --- a/node/tests/PubSub.test.ts +++ b/node/tests/PubSub.test.ts @@ -2970,7 +2970,7 @@ describe("PubSub", () => { expect( await (publishingClient as GlideClusterClient).publish( - message, + Buffer.from(message), channel, true, ), @@ -2979,7 +2979,7 @@ describe("PubSub", () => { expect( await (publishingClient as GlideClusterClient).publish( message2, - channel, + Buffer.from(channel), true, ), ).toEqual(1); @@ -3352,15 +3352,17 @@ describe("PubSub", () => { ); // Test pubsubChannels with pattern - const channelsWithPattern = - await client2.pubsubChannels(pattern); + const channelsWithPattern = await client2.pubsubChannels({ + pattern, + }); expect(new Set(channelsWithPattern)).toEqual( new Set([channel1, channel2]), ); // Test with non-matching pattern - const nonMatchingChannels = - await client2.pubsubChannels("non_matching_*"); + const nonMatchingChannels = await client2.pubsubChannels({ + pattern: "non_matching_*", + }); expect(nonMatchingChannels.length).toBe(0); } finally { if (client1) { @@ -3695,7 +3697,7 @@ describe("PubSub", () => { // Test pubsubShardchannels with pattern const channelsWithPattern = await ( client2 as GlideClusterClient - ).pubsubShardChannels(pattern); + ).pubsubShardChannels({ pattern }); expect(new Set(channelsWithPattern)).toEqual( new Set([channel1, channel2]), ); @@ -3703,7 +3705,7 @@ describe("PubSub", () => { // Test with non-matching pattern const nonMatchingChannels = await ( client2 as GlideClusterClient - ).pubsubShardChannels("non_matching_*"); + ).pubsubShardChannels({ pattern: "non_matching_*" }); expect(nonMatchingChannels).toEqual([]); } finally { if (client1) { From 015d5a993a91fe719e57bd6394d1addf04e16721 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:11:55 -0700 Subject: [PATCH 232/236] Node: add binary support to string commands (#2183) * Node: add binary support to string commands Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 83 ++++++++++++++++++++++++++------------- node/src/Commands.ts | 20 +++++----- node/src/Transaction.ts | 29 +++++++------- node/tests/SharedTests.ts | 60 ++++++++++++++++++++++------ 5 files changed, 129 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7639dba5b..78bb75ed77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ * Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) * Node: Added XACK commands ([#2112](https://github.com/valkey-io/valkey-glide/pull/2112)) * Node: Added XGROUP SETID command ([#2135]((https://github.com/valkey-io/valkey-glide/pull/2135)) +* Node: Added binary variant to string commands ([#2183](https://github.com/valkey-io/valkey-glide/pull/2183)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 772e1e904b..0e14bcfe12 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -987,9 +987,11 @@ export class BaseClient { * @remarks Since Valkey version 6.2.0. * * @param key - The key to retrieve from the database. - * @param options - (Optional) Set expiriation to the given key. - * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. - * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * @param options - (Optional) Additional Parameters: + * - (Optional) `expiry`: expiriation to the given key: + * `"persist"` will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. + * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. * * @example @@ -999,10 +1001,14 @@ export class BaseClient { * ``` */ public async getex( - key: string, - options?: "persist" | { type: TimeUnit; duration: number }, - ): Promise { - return this.createWritePromise(createGetEx(key, options)); + key: GlideString, + options?: { + expiry: "persist" | { type: TimeUnit; duration: number }; + } & DecoderOption, + ): Promise { + return this.createWritePromise(createGetEx(key, options?.expiry), { + decoder: options?.decoder, + }); } /** @@ -1076,7 +1082,7 @@ export class BaseClient { * * @param key - The key to store. * @param value - The value to store with the given key. - * @param options - The set options. + * @param options - (Optional) See {@link SetOptions} and {@link DecoderOption}. * @returns - If the value is successfully set, return OK. * If value isn't set because of `onlyIfExists` or `onlyIfDoesNotExist` conditions, return null. * If `returnOldValue` is set, return the old value as a string. @@ -1103,9 +1109,11 @@ export class BaseClient { public async set( key: GlideString, value: GlideString, - options?: SetOptions, - ): Promise<"OK" | string | null> { - return this.createWritePromise(createSet(key, value, options)); + options?: SetOptions & DecoderOption, + ): Promise<"OK" | GlideString | null> { + return this.createWritePromise(createSet(key, value, options), { + decoder: options?.decoder, + }); } /** @@ -1294,7 +1302,7 @@ export class BaseClient { * console.log(result); // Output: 11 * ``` */ - public async incr(key: string): Promise { + public async incr(key: GlideString): Promise { return this.createWritePromise(createIncr(key)); } @@ -1314,7 +1322,7 @@ export class BaseClient { * console.log(result); // Output: 15 * ``` */ - public async incrBy(key: string, amount: number): Promise { + public async incrBy(key: GlideString, amount: number): Promise { return this.createWritePromise(createIncrBy(key, amount)); } @@ -1336,7 +1344,10 @@ export class BaseClient { * console.log(result); // Output: 13.0 * ``` */ - public async incrByFloat(key: string, amount: number): Promise { + public async incrByFloat( + key: GlideString, + amount: number, + ): Promise { return this.createWritePromise(createIncrByFloat(key, amount)); } @@ -1355,7 +1366,7 @@ export class BaseClient { * console.log(result); // Output: 9 * ``` */ - public async decr(key: string): Promise { + public async decr(key: GlideString): Promise { return this.createWritePromise(createDecr(key)); } @@ -1375,7 +1386,7 @@ export class BaseClient { * console.log(result); // Output: 5 * ``` */ - public async decrBy(key: string, amount: number): Promise { + public async decrBy(key: GlideString, amount: number): Promise { return this.createWritePromise(createDecrBy(key, amount)); } @@ -6319,6 +6330,7 @@ export class BaseClient { * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. + * @param options - (Optional) See {@link DecoderOption}. * @returns A `String` containing all the longest common subsequence combined between the 2 strings. * An empty `String` is returned if the keys do not exist or have no common subsequences. * @@ -6329,8 +6341,12 @@ export class BaseClient { * console.log(result); // Output: 'acd' * ``` */ - public async lcs(key1: string, key2: string): Promise { - return this.createWritePromise(createLCS(key1, key2)); + public async lcs( + key1: GlideString, + key2: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createLCS(key1, key2), options); } /** @@ -6342,6 +6358,7 @@ export class BaseClient { * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. + * @param options - (Optional) See {@link DecoderOption}. * @returns The total length of all the longest common subsequences between the 2 strings. * * @example @@ -6351,8 +6368,15 @@ export class BaseClient { * console.log(result); // Output: 3 * ``` */ - public async lcsLen(key1: string, key2: string): Promise { - return this.createWritePromise(createLCS(key1, key2, { len: true })); + public async lcsLen( + key1: GlideString, + key2: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise( + createLCS(key1, key2, { len: true }), + options, + ); } /** @@ -6365,8 +6389,9 @@ export class BaseClient { * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. - * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match. - * @param minMatchLen - (Optional) The minimum length of matches to include in the result. + * @param options - (Optional) Additional parameters: + * - (Optional) `withMatchLen`: if `true`, include the length of the substring matched for the each match. + * - (Optional) `minMatchLen`: the minimum length of matches to include in the result. * @returns A `Record` containing the indices of the longest common subsequences between the * 2 strings and the lengths of the longest common subsequences. The resulting map contains two * keys, "matches" and "len": @@ -6402,12 +6427,16 @@ export class BaseClient { * ``` */ public async lcsIdx( - key1: string, - key2: string, - options?: { withMatchLen?: boolean; minMatchLen?: number }, + key1: GlideString, + key2: GlideString, + options?: { + withMatchLen?: boolean; + minMatchLen?: number; + }, ): Promise> { return this.createWritePromise( createLCS(key1, key2, { idx: options ?? {} }), + { decoder: Decoder.String }, ); } @@ -6509,9 +6538,9 @@ export class BaseClient { * ``` */ public async setrange( - key: string, + key: GlideString, offset: number, - value: string, + value: GlideString, ): Promise { return this.createWritePromise(createSetRange(key, offset, value)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 78fe767852..41c8eea946 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -348,7 +348,7 @@ export function createMSetNX( /** * @internal */ -export function createIncr(key: string): command_request.Command { +export function createIncr(key: GlideString): command_request.Command { return createCommand(RequestType.Incr, [key]); } @@ -356,7 +356,7 @@ export function createIncr(key: string): command_request.Command { * @internal */ export function createIncrBy( - key: string, + key: GlideString, amount: number, ): command_request.Command { return createCommand(RequestType.IncrBy, [key, amount.toString()]); @@ -366,7 +366,7 @@ export function createIncrBy( * @internal */ export function createIncrByFloat( - key: string, + key: GlideString, amount: number, ): command_request.Command { return createCommand(RequestType.IncrByFloat, [key, amount.toString()]); @@ -442,7 +442,7 @@ export function createHSetNX( /** * @internal */ -export function createDecr(key: string): command_request.Command { +export function createDecr(key: GlideString): command_request.Command { return createCommand(RequestType.Decr, [key]); } @@ -450,7 +450,7 @@ export function createDecr(key: string): command_request.Command { * @internal */ export function createDecrBy( - key: string, + key: GlideString, amount: number, ): command_request.Command { return createCommand(RequestType.DecrBy, [key, amount.toString()]); @@ -3681,8 +3681,8 @@ export function createLastSave(): command_request.Command { /** @internal */ export function createLCS( - key1: string, - key2: string, + key1: GlideString, + key2: GlideString, options?: { len?: boolean; idx?: { withMatchLen?: boolean; minMatchLen?: number }; @@ -3794,9 +3794,9 @@ export function createZScan( /** @internal */ export function createSetRange( - key: string, + key: GlideString, offset: number, - value: string, + value: GlideString, ): command_request.Command { return createCommand(RequestType.SetRange, [key, offset.toString(), value]); } @@ -3944,7 +3944,7 @@ export enum TimeUnit { * @internal */ export function createGetEx( - key: string, + key: GlideString, options?: "persist" | { type: TimeUnit; duration: number }, ): command_request.Command { const args = [key]; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index eabf799f1b..0422fdee09 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -337,7 +337,7 @@ export class BaseTransaction> { * Command Response - If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. */ public getex( - key: string, + key: GlideString, options?: "persist" | { type: TimeUnit; duration: number }, ): T { return this.addAndReturn(createGetEx(key, options)); @@ -386,7 +386,7 @@ export class BaseTransaction> { * If `value` isn't set because of `onlyIfExists` or `onlyIfDoesNotExist` conditions, return null. * If `returnOldValue` is set, return the old value as a string. */ - public set(key: string, value: string, options?: SetOptions): T { + public set(key: GlideString, value: GlideString, options?: SetOptions): T { return this.addAndReturn(createSet(key, value, options)); } @@ -545,7 +545,7 @@ export class BaseTransaction> { * * Command Response - the value of `key` after the increment. */ - public incr(key: string): T { + public incr(key: GlideString): T { return this.addAndReturn(createIncr(key)); } @@ -557,7 +557,7 @@ export class BaseTransaction> { * * Command Response - the value of `key` after the increment. */ - public incrBy(key: string, amount: number): T { + public incrBy(key: GlideString, amount: number): T { return this.addAndReturn(createIncrBy(key, amount)); } @@ -572,7 +572,7 @@ export class BaseTransaction> { * Command Response - the value of `key` after the increment. * */ - public incrByFloat(key: string, amount: number): T { + public incrByFloat(key: GlideString, amount: number): T { return this.addAndReturn(createIncrByFloat(key, amount)); } @@ -594,7 +594,7 @@ export class BaseTransaction> { * * Command Response - the value of `key` after the decrement. */ - public decr(key: string): T { + public decr(key: GlideString): T { return this.addAndReturn(createDecr(key)); } @@ -606,7 +606,7 @@ export class BaseTransaction> { * * Command Response - the value of `key` after the decrement. */ - public decrBy(key: string, amount: number): T { + public decrBy(key: GlideString, amount: number): T { return this.addAndReturn(createDecrBy(key, amount)); } @@ -3679,7 +3679,7 @@ export class BaseTransaction> { * Command Response - A `String` containing all the longest common subsequence combined between the 2 strings. * An empty `String` is returned if the keys do not exist or have no common subsequences. */ - public lcs(key1: string, key2: string): T { + public lcs(key1: GlideString, key2: GlideString): T { return this.addAndReturn(createLCS(key1, key2)); } @@ -3694,7 +3694,7 @@ export class BaseTransaction> { * * Command Response - The total length of all the longest common subsequences between the 2 strings. */ - public lcsLen(key1: string, key2: string): T { + public lcsLen(key1: GlideString, key2: GlideString): T { return this.addAndReturn(createLCS(key1, key2, { len: true })); } @@ -3707,8 +3707,9 @@ export class BaseTransaction> { * * @param key1 - The key that stores the first string. * @param key2 - The key that stores the second string. - * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match. - * @param minMatchLen - (Optional) The minimum length of matches to include in the result. + * @param options - (Optional) Additional parameters: + * - (Optional) `withMatchLen`: if `true`, include the length of the substring matched for the each match. + * - (Optional) `minMatchLen`: the minimum length of matches to include in the result. * * Command Response - A `Record` containing the indices of the longest common subsequences between the * 2 strings and the lengths of the longest common subsequences. The resulting map contains two @@ -3722,8 +3723,8 @@ export class BaseTransaction> { * See example of {@link BaseClient.lcsIdx|lcsIdx} for more details. */ public lcsIdx( - key1: string, - key2: string, + key1: GlideString, + key2: GlideString, options?: { withMatchLen?: boolean; minMatchLen?: number }, ): T { return this.addAndReturn(createLCS(key1, key2, { idx: options ?? {} })); @@ -3766,7 +3767,7 @@ export class BaseTransaction> { * * Command Response - The length of the string stored at `key` after it was modified. */ - public setrange(key: string, offset: number, value: string): T { + public setrange(key: GlideString, offset: number, value: GlideString): T { return this.addAndReturn(createSetRange(key, offset, value)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c3b82e046b..0fb3074630 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -438,13 +438,17 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); + const keyEncoded = Buffer.from(key); expect(await client.set(key, "10")).toEqual("OK"); expect(await client.incr(key)).toEqual(11); - expect(await client.get(key)).toEqual("11"); - expect(await client.incrBy(key, 4)).toEqual(15); - expect(await client.get(key)).toEqual("15"); - expect(await client.incrByFloat(key, 1.5)).toEqual(16.5); - expect(await client.get(key)).toEqual("16.5"); + expect(await client.incr(keyEncoded)).toEqual(12); + expect(await client.get(key)).toEqual("12"); + expect(await client.incrBy(key, 4)).toEqual(16); + expect(await client.incrBy(keyEncoded, 1)).toEqual(17); + expect(await client.get(key)).toEqual("17"); + expect(await client.incrByFloat(key, 1.5)).toEqual(18.5); + expect(await client.incrByFloat(key, 1.5)).toEqual(20); + expect(await client.get(key)).toEqual("20"); }, protocol); }, config.timeout, @@ -550,11 +554,14 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); + const keyEncoded = Buffer.from(key); expect(await client.set(key, "10")).toEqual("OK"); expect(await client.decr(key)).toEqual(9); - expect(await client.get(key)).toEqual("9"); - expect(await client.decrBy(key, 4)).toEqual(5); - expect(await client.get(key)).toEqual("5"); + expect(await client.decr(keyEncoded)).toEqual(8); + expect(await client.get(key)).toEqual("8"); + expect(await client.decrBy(key, 4)).toEqual(4); + expect(await client.decrBy(keyEncoded, 1)).toEqual(3); + expect(await client.get(key)).toEqual("3"); }, protocol); }, config.timeout, @@ -9679,11 +9686,23 @@ export function runBaseTests(config: { // keys does not exist or is empty expect(await client.lcs(key1, key2)).toEqual(""); + expect( + await client.lcs(Buffer.from(key1), Buffer.from(key2)), + ).toEqual(""); expect(await client.lcsLen(key1, key2)).toEqual(0); + expect( + await client.lcsLen(Buffer.from(key1), Buffer.from(key2)), + ).toEqual(0); expect(await client.lcsIdx(key1, key2)).toEqual({ matches: [], len: 0, }); + expect( + await client.lcsIdx(Buffer.from(key1), Buffer.from(key2)), + ).toEqual({ + matches: [], + len: 0, + }); // LCS with some strings expect( @@ -11203,12 +11222,25 @@ export function runBaseTests(config: { expect( await client.getex(key1, { - type: TimeUnit.Seconds, - duration: 15, + expiry: { + type: TimeUnit.Seconds, + duration: 15, + }, + }), + ).toEqual(value); + // test the binary option + expect( + await client.getex(Buffer.from(key1), { + expiry: { + type: TimeUnit.Seconds, + duration: 1, + }, }), ).toEqual(value); expect(await client.ttl(key1)).toBeGreaterThan(0); - expect(await client.getex(key1, "persist")).toEqual(value); + expect(await client.getex(key1, { expiry: "persist" })).toEqual( + value, + ); expect(await client.ttl(key1)).toBe(-1); // non existent key @@ -11217,8 +11249,10 @@ export function runBaseTests(config: { // invalid time measurement await expect( client.getex(key1, { - type: TimeUnit.Seconds, - duration: -10, + expiry: { + type: TimeUnit.Seconds, + duration: -10, + }, }), ).rejects.toThrow(RequestError); From 95653139df214b750828fc9280b073df7418269a Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 28 Aug 2024 13:19:48 -0700 Subject: [PATCH 233/236] Node: Add binary variant to sorted set commands - part 1. (#2190) * Add binary variant to sorted set commands - part 1. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 141 ++++++++++++++++++++++++-------------- node/src/Commands.ts | 61 +++++++++-------- node/src/Transaction.ts | 73 +++++++++++--------- node/tests/SharedTests.ts | 136 +++++++++++++++++++++++++----------- 5 files changed, 259 insertions(+), 153 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78bb75ed77..f9a1decdad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant to sorted set commands - part 1 ([#2190](https://github.com/valkey-io/valkey-glide/pull/2190)) * Node: Added binary variant to HASH commands ([#2194](https://github.com/valkey-io/valkey-glide/pull/2194)) * Node: Added binary variant to server management commands ([#2179](https://github.com/valkey-io/valkey-glide/pull/2179)) * Node: Added/updated binary variant to connection management commands and WATCH/UNWATCH ([#2160](https://github.com/valkey-io/valkey-glide/pull/2160)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0e14bcfe12..ce8afe3112 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -3337,7 +3337,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/ttl/|valkey.io} for details. * * @param key - The key to return its timeout. - * @returns TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. + * @returns TTL in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * * @example * ```typescript @@ -3495,16 +3495,17 @@ export class BaseClient { return this.createWritePromise(createXRevRange(key, end, start, count)); } - /** Adds members with their scores to the sorted set stored at `key`. + /** + * Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. * * @see {@link https://valkey.io/commands/zadd/|valkey.io} for more details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. - * @param options - The ZAdd options. + * @param options - (Optional) The ZAdd options - see {@link ZAddOptions}. * @returns The number of elements added to the sorted set. - * If `changed` is set, returns the number of elements updated in the sorted set. + * If {@link ZAddOptions.changed|changed} is set, returns the number of elements updated in the sorted set. * * @example * ```typescript @@ -3531,7 +3532,8 @@ export class BaseClient { ); } - /** Increments the score of member in the sorted set stored at `key` by `increment`. + /** + * Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. * @@ -3540,7 +3542,7 @@ export class BaseClient { * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. * @param increment - The score to increment the member. - * @param options - The ZAdd options. + * @param options - (Optional) The ZAdd options - see {@link ZAddOptions}. * @returns The score of the member. * If there was a conflict with the options, the operation aborts and null is returned. * @@ -3597,13 +3599,14 @@ export class BaseClient { return this.createWritePromise(createZRem(key, members)); } - /** Returns the cardinality (number of elements) of the sorted set stored at `key`. + /** + * Returns the cardinality (number of elements) of the sorted set stored at `key`. * * @see {@link https://valkey.io/commands/zcard/|valkey.io} for more details. * * @param key - The key of the sorted set. * @returns The number of elements in the sorted set. - * If `key` does not exist, it is treated as an empty sorted set, and this command returns 0. + * If `key` does not exist, it is treated as an empty sorted set, and this command returns `0`. * * @example * ```typescript @@ -3619,7 +3622,7 @@ export class BaseClient { * console.log(result); // Output: 0 * ``` */ - public async zcard(key: string): Promise { + public async zcard(key: GlideString): Promise { return this.createWritePromise(createZCard(key)); } @@ -3641,7 +3644,10 @@ export class BaseClient { * console.log(cardinality); // Output: 3 - The intersection of the sorted sets at "key1" and "key2" has a cardinality of 3. * ``` */ - public async zintercard(keys: string[], limit?: number): Promise { + public async zintercard( + keys: GlideString[], + limit?: number, + ): Promise { return this.createWritePromise(createZInterCard(keys, limit)); } @@ -3654,6 +3660,7 @@ export class BaseClient { * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets. + * @param options - (Optional) See {@link DecoderOption}. * @returns An `array` of elements representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. * @@ -3666,8 +3673,11 @@ export class BaseClient { * console.log(result); // Output: ["member1"] - "member1" is in "zset1" but not "zset2" or "zset3". * ``` */ - public async zdiff(keys: string[]): Promise { - return this.createWritePromise(createZDiff(keys)); + public async zdiff( + keys: GlideString[], + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createZDiff(keys), options); } /** @@ -3679,6 +3689,7 @@ export class BaseClient { * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets. + * @param options - (Optional) See {@link DecoderOption}. * @returns A map of elements and their scores representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. * @@ -3692,9 +3703,11 @@ export class BaseClient { * ``` */ public async zdiffWithScores( - keys: string[], + keys: GlideString[], + options?: DecoderOption, ): Promise> { - return this.createWritePromise(createZDiffWithScores(keys)); + // TODO GlideString in Record and add a test + return this.createWritePromise(createZDiffWithScores(keys), options); } /** @@ -3722,8 +3735,8 @@ export class BaseClient { * ``` */ public async zdiffstore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): Promise { return this.createWritePromise(createZDiffStore(destination, keys)); } @@ -3823,7 +3836,8 @@ export class BaseClient { return this.createWritePromise(createZMScore(key, members)); } - /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. + /** + * Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. * * @see {@link https://valkey.io/commands/zcount/|valkey.io} for more details. * @@ -3831,8 +3845,8 @@ export class BaseClient { * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. * @param maxScore - The maximum score to count up to. Can be positive/negative infinity, or specific score and inclusivity. * @returns The number of members in the specified score range. - * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - * If `minScore` is greater than `maxScore`, 0 is returned. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + * If `minScore` is greater than `maxScore`, `0` is returned. * * @example * ```typescript @@ -3849,7 +3863,7 @@ export class BaseClient { * ``` */ public async zcount( - key: string, + key: GlideString, minScore: Boundary, maxScore: Boundary, ): Promise { @@ -3998,26 +4012,33 @@ export class BaseClient { * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: - * string[] - for keys only. - * KeyWeight[] - for weighted keys with score multipliers. + * - `GlideString[]` - for keys only. + * - `KeyWeight[]` - for weighted keys with score multipliers. * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * @returns The number of elements in the resulting sorted set stored at `destination`. * * @example * ```typescript - * // Example usage of zinterstore command with an existing key * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) * await client.zadd("key2", {"member1": 9.5}) - * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. - * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. - * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. - * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * + * // use `zinterstore` with default aggregation and weights + * console.log(await client.zinterstore("my_sorted_set", ["key1", "key2"])) + * // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. + * console.log(await client.zrangeWithScores("my_sorted_set", {start: 0, stop: -1})) + * // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * + * // use `zinterstore` with default weights + * console.log(await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX)) + * // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. + * console.log(await client.zrangeWithScores("my_sorted_set", {start: 0, stop: -1})) + * // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. * ``` */ public async zinterstore( - destination: string, - keys: string[] | KeyWeight[], + destination: GlideString, + keys: GlideString[] | KeyWeight[], aggregationType?: AggregationType, ): Promise { return this.createWritePromise( @@ -4037,6 +4058,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. * * @param keys - The keys of the sorted sets. + * @param options - (Optional) See {@link DecoderOption}. * @returns The resulting array of intersecting elements. * * @example @@ -4047,8 +4069,11 @@ export class BaseClient { * console.log(result); // Output: ['member1'] * ``` */ - public async zinter(keys: string[]): Promise { - return this.createWritePromise(createZInter(keys)); + public async zinter( + keys: GlideString[], + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createZInter(keys), options); } /** @@ -4063,10 +4088,12 @@ export class BaseClient { * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets with possible formats: - * - string[] - for keys only. - * - KeyWeight[] - for weighted keys with score multipliers. - * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. - * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * - `GlideString[]` - for keys only. + * - `KeyWeight[]` - for weighted keys with score multipliers. + * @param options - (Optional) Additional parameters: + * - (Optional) `aggregationType`: the aggregation strategy to apply when combining the scores of elements. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. See {@link AggregationType}. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns The resulting sorted set with scores. * * @example @@ -4080,11 +4107,13 @@ export class BaseClient { * ``` */ public async zinterWithScores( - keys: string[] | KeyWeight[], - aggregationType?: AggregationType, + keys: GlideString[] | KeyWeight[], + options?: { aggregationType?: AggregationType } & DecoderOption, ): Promise> { + // TODO Record with GlideString and add tests return this.createWritePromise( - createZInter(keys, aggregationType, true), + createZInter(keys, options?.aggregationType, true), + options, ); } @@ -4341,6 +4370,7 @@ export class BaseClient { * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * @param options - (Optional) See {@link DecoderOption}. * @returns An `array` containing the key where the member was popped out, the member, itself, and the member score. * If no member could be popped and the `timeout` expired, returns `null`. * @@ -4351,10 +4381,11 @@ export class BaseClient { * ``` */ public async bzpopmin( - keys: string[], + keys: GlideString[], timeout: number, - ): Promise<[string, string, number] | null> { - return this.createWritePromise(createBZPopMin(keys, timeout)); + options?: DecoderOption, + ): Promise<[GlideString, GlideString, number] | null> { + return this.createWritePromise(createBZPopMin(keys, timeout), options); } /** Removes and returns the members with the highest scores from the sorted set stored at `key`. @@ -4402,6 +4433,7 @@ export class BaseClient { * @param keys - The keys of the sorted sets. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of * `0` will block indefinitely. Since 6.0.0: timeout is interpreted as a double instead of an integer. + * @param options - (Optional) See {@link DecoderOption}. * @returns An `array` containing the key where the member was popped out, the member, itself, and the member score. * If no member could be popped and the `timeout` expired, returns `null`. * @@ -4412,10 +4444,11 @@ export class BaseClient { * ``` */ public async bzpopmax( - keys: string[], + keys: GlideString[], timeout: number, - ): Promise<[string, string, number] | null> { - return this.createWritePromise(createBZPopMax(keys, timeout)); + options?: DecoderOption, + ): Promise<[GlideString, GlideString, number] | null> { + return this.createWritePromise(createBZPopMax(keys, timeout), options); } /** @@ -4424,7 +4457,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/pttl/|valkey.io} for more details. * * @param key - The key to return its timeout. - * @returns TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + * @returns TTL in milliseconds, `-2` if `key` does not exist, `-1` if `key` exists but has no associated expire. * * @example * ```typescript @@ -6163,7 +6196,9 @@ export class BaseClient { * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. * @param timeout - The number of seconds to wait for a blocking operation to complete. * A value of 0 will block indefinitely. - * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the number of elements to pop. If not supplied, only one element will be popped. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A two-element `array` containing the key name of the set from which the element * was popped, and a member-score `Record` of the popped element. * If no member could be popped, returns `null`. @@ -6177,13 +6212,15 @@ export class BaseClient { * ``` */ public async bzmpop( - keys: string[], + keys: GlideString[], modifier: ScoreFilter, timeout: number, - count?: number, - ): Promise<[string, [Record]] | null> { + options?: { count?: number } & DecoderOption, + ): Promise<[string, Record] | null> { + // TODO GlideString in Record return this.createWritePromise( - createBZMPop(keys, modifier, timeout, count), + createBZMPop(keys, modifier, timeout, options?.count), + options, ); } @@ -6213,9 +6250,9 @@ export class BaseClient { * ``` */ public async zincrby( - key: string, + key: GlideString, increment: number, - member: string, + member: GlideString, ): Promise { return this.createWritePromise(createZIncrBy(key, increment, member)); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 41c8eea946..f3d6d934d1 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1421,7 +1421,7 @@ export function createZAdd( /** * `KeyWeight` - pair of variables represents a weighted key for the `ZINTERSTORE` and `ZUNIONSTORE` sorted sets commands. */ -export type KeyWeight = [string, number]; +export type KeyWeight = [GlideString, number]; /** * `AggregationType` - representing aggregation types for `ZINTERSTORE` and `ZUNIONSTORE` sorted set commands. */ @@ -1431,8 +1431,8 @@ export type AggregationType = "SUM" | "MIN" | "MAX"; * @internal */ export function createZInterstore( - destination: string, - keys: string[] | KeyWeight[], + destination: GlideString, + keys: GlideString[] | KeyWeight[], aggregationType?: AggregationType, ): command_request.Command { const args = createZCmdArgs(keys, { @@ -1447,7 +1447,7 @@ export function createZInterstore( * @internal */ export function createZInter( - keys: string[] | KeyWeight[], + keys: GlideString[] | KeyWeight[], aggregationType?: AggregationType, withScores?: boolean, ): command_request.Command { @@ -1472,14 +1472,14 @@ export function createZUnion( * Helper function for Zcommands (ZInter, ZinterStore, ZUnion..) that arranges arguments in the server's required order. */ function createZCmdArgs( - keys: string[] | KeyWeight[], + keys: GlideString[] | KeyWeight[], options: { aggregationType?: AggregationType; withScores?: boolean; - destination?: string; + destination?: GlideString; }, -): string[] { - const args = []; +): GlideString[] { + const args: GlideString[] = []; const destination = options.destination; @@ -1489,11 +1489,12 @@ function createZCmdArgs( args.push(keys.length.toString()); - if (typeof keys[0] === "string") { - args.push(...(keys as string[])); + if (!Array.isArray(keys[0])) { + // KeyWeight is an array + args.push(...(keys as GlideString[])); } else { const weightsKeys = keys.map(([key]) => key); - args.push(...(weightsKeys as string[])); + args.push(...(weightsKeys as GlideString[])); const weights = keys.map(([, weight]) => weight.toString()); args.push("WEIGHTS", ...weights); } @@ -1524,7 +1525,7 @@ export function createZRem( /** * @internal */ -export function createZCard(key: string): command_request.Command { +export function createZCard(key: GlideString): command_request.Command { return createCommand(RequestType.ZCard, [key]); } @@ -1532,14 +1533,14 @@ export function createZCard(key: string): command_request.Command { * @internal */ export function createZInterCard( - keys: string[], + keys: GlideString[], limit?: number, ): command_request.Command { - let args: string[] = keys; + const args = keys; args.unshift(keys.length.toString()); if (limit != undefined) { - args = args.concat(["LIMIT", limit.toString()]); + args.push("LIMIT", limit.toString()); } return createCommand(RequestType.ZInterCard, args); @@ -1548,8 +1549,8 @@ export function createZInterCard( /** * @internal */ -export function createZDiff(keys: string[]): command_request.Command { - const args: string[] = keys; +export function createZDiff(keys: GlideString[]): command_request.Command { + const args = keys; args.unshift(keys.length.toString()); return createCommand(RequestType.ZDiff, args); } @@ -1557,8 +1558,10 @@ export function createZDiff(keys: string[]): command_request.Command { /** * @internal */ -export function createZDiffWithScores(keys: string[]): command_request.Command { - const args: string[] = keys; +export function createZDiffWithScores( + keys: GlideString[], +): command_request.Command { + const args = keys; args.unshift(keys.length.toString()); args.push("WITHSCORES"); return createCommand(RequestType.ZDiff, args); @@ -1568,10 +1571,10 @@ export function createZDiffWithScores(keys: string[]): command_request.Command { * @internal */ export function createZDiffStore( - destination: string, - keys: string[], + destination: GlideString, + keys: GlideString[], ): command_request.Command { - const args: string[] = [destination, keys.length.toString(), ...keys]; + const args = [destination, keys.length.toString(), ...keys]; return createCommand(RequestType.ZDiffStore, args); } @@ -1790,7 +1793,7 @@ function createZRangeArgs( * @internal */ export function createZCount( - key: string, + key: GlideString, minScore: Boundary, maxScore: Boundary, ): command_request.Command { @@ -3455,12 +3458,12 @@ export function createZMPop( * @internal */ export function createBZMPop( - keys: string[], + keys: GlideString[], modifier: ScoreFilter, timeout: number, count?: number, ): command_request.Command { - const args: string[] = [ + const args = [ timeout.toString(), keys.length.toString(), ...keys, @@ -3479,9 +3482,9 @@ export function createBZMPop( * @internal */ export function createZIncrBy( - key: string, + key: GlideString, increment: number, - member: string, + member: GlideString, ): command_request.Command { return createCommand(RequestType.ZIncrBy, [ key, @@ -3898,7 +3901,7 @@ export function createPubSubShardNumSub( * @internal */ export function createBZPopMax( - keys: string[], + keys: GlideString[], timeout: number, ): command_request.Command { return createCommand(RequestType.BZPopMax, [...keys, timeout.toString()]); @@ -3908,7 +3911,7 @@ export function createBZPopMax( * @internal */ export function createBZPopMin( - keys: string[], + keys: GlideString[], timeout: number, ): command_request.Command { return createCommand(RequestType.BZPopMin, [...keys, timeout.toString()]); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 0422fdee09..c338d01923 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1711,22 +1711,24 @@ export class BaseTransaction> { * * @param key - The key to return its timeout. * - * Command Response - TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. + * Command Response - TTL in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ public ttl(key: GlideString): T { return this.addAndReturn(createTTL(key)); } - /** Adds members with their scores to the sorted set stored at `key`. + /** + * Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. + * * @see {@link https://valkey.io/commands/zadd/|valkey.io} for details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. - * @param options - The ZAdd options. + * @param options - (Optional) The ZAdd options - see {@link ZAddOptions}. * * Command Response - The number of elements added to the sorted set. - * If `changed` is set, returns the number of elements updated in the sorted set. + * If {@link ZAddOptions.changed|changed} is set, returns the number of elements updated in the sorted set. */ public zadd( key: string, @@ -1736,7 +1738,8 @@ export class BaseTransaction> { return this.addAndReturn(createZAdd(key, membersScoresMap, options)); } - /** Increments the score of member in the sorted set stored at `key` by `increment`. + /** + * Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. * @see {@link https://valkey.io/commands/zadd/|valkey.io} for details. @@ -1744,7 +1747,7 @@ export class BaseTransaction> { * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. * @param increment - The score to increment the member. - * @param options - The ZAdd options. + * @param options - (Optional) The ZAdd options - see {@link ZAddOptions}. * * Command Response - The score of the member. * If there was a conflict with the options, the operation aborts and null is returned. @@ -1774,15 +1777,17 @@ export class BaseTransaction> { return this.addAndReturn(createZRem(key, members)); } - /** Returns the cardinality (number of elements) of the sorted set stored at `key`. + /** + * Returns the cardinality (number of elements) of the sorted set stored at `key`. + * * @see {@link https://valkey.io/commands/zcard/|valkey.io} for details. * * @param key - The key of the sorted set. * * Command Response - The number of elements in the sorted set. - * If `key` does not exist, it is treated as an empty sorted set, and this command returns 0. + * If `key` does not exist, it is treated as an empty sorted set, and this command returns `0`. */ - public zcard(key: string): T { + public zcard(key: GlideString): T { return this.addAndReturn(createZCard(key)); } @@ -1798,7 +1803,7 @@ export class BaseTransaction> { * * Command Response - The cardinality of the intersection of the given sorted sets. */ - public zintercard(keys: string[], limit?: number): T { + public zintercard(keys: GlideString[], limit?: number): T { return this.addAndReturn(createZInterCard(keys, limit)); } @@ -1814,7 +1819,7 @@ export class BaseTransaction> { * Command Response - An `array` of elements representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. */ - public zdiff(keys: string[]): T { + public zdiff(keys: GlideString[]): T { return this.addAndReturn(createZDiff(keys)); } @@ -1830,7 +1835,7 @@ export class BaseTransaction> { * Command Response - A map of elements and their scores representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. */ - public zdiffWithScores(keys: string[]): T { + public zdiffWithScores(keys: GlideString[]): T { return this.addAndReturn(createZDiffWithScores(keys)); } @@ -1847,7 +1852,7 @@ export class BaseTransaction> { * * Command Response - The number of members in the resulting sorted set stored at `destination`. */ - public zdiffstore(destination: string, keys: string[]): T { + public zdiffstore(destination: GlideString, keys: GlideString[]): T { return this.addAndReturn(createZDiffStore(destination, keys)); } @@ -1905,7 +1910,9 @@ export class BaseTransaction> { return this.addAndReturn(createZMScore(key, members)); } - /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. + /** + * Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. + * * @see {@link https://valkey.io/commands/zcount/|valkey.io} for details. * * @param key - The key of the sorted set. @@ -1913,11 +1920,11 @@ export class BaseTransaction> { * @param maxScore - The maximum score to count up to. Can be positive/negative infinity, or specific score and inclusivity. * * Command Response - The number of members in the specified score range. - * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - * If `minScore` is greater than `maxScore`, 0 is returned. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + * If `minScore` is greater than `maxScore`, `0` is returned. */ public zcount( - key: string, + key: GlideString, minScore: Boundary, maxScore: Boundary, ): T { @@ -2005,24 +2012,22 @@ export class BaseTransaction> { * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. * - * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. - * * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for details. * * @remarks Since Valkey version 6.2.0. * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: - * string[] - for keys only. - * KeyWeight[] - for weighted keys with score multipliers. + * - `GlideString[]` - for keys only. + * - `KeyWeight[]` - for weighted keys with score multipliers. * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * * Command Response - The number of elements in the resulting sorted set stored at `destination`. */ public zinterstore( - destination: string, - keys: string[] | KeyWeight[], + destination: GlideString, + keys: GlideString[] | KeyWeight[], aggregationType?: AggregationType, ): T { return this.addAndReturn( @@ -2043,7 +2048,7 @@ export class BaseTransaction> { * * Command Response - The resulting array of intersecting elements. */ - public zinter(keys: string[]): T { + public zinter(keys: GlideString[]): T { return this.addAndReturn(createZInter(keys)); } @@ -2057,15 +2062,15 @@ export class BaseTransaction> { * @remarks Since Valkey version 6.2.0. * * @param keys - The keys of the sorted sets with possible formats: - * - string[] - for keys only. - * - KeyWeight[] - for weighted keys with score multipliers. + * - `GlideString[]` - for keys only. + * - `KeyWeight[]` - for weighted keys with score multipliers. * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * * Command Response - The resulting sorted set with scores. */ public zinterWithScores( - keys: string[] | KeyWeight[], + keys: GlideString[] | KeyWeight[], aggregationType?: AggregationType, ): T { return this.addAndReturn(createZInter(keys, aggregationType, true)); @@ -2214,7 +2219,7 @@ export class BaseTransaction> { * Command Response - An `array` containing the key where the member was popped out, the member, itself, and the member score. * If no member could be popped and the `timeout` expired, returns `null`. */ - public bzpopmin(keys: string[], timeout: number): T { + public bzpopmin(keys: GlideString[], timeout: number): T { return this.addAndReturn(createBZPopMin(keys, timeout)); } @@ -2249,7 +2254,7 @@ export class BaseTransaction> { * Command Response - An `array` containing the key where the member was popped out, the member, itself, and the member score. * If no member could be popped and the `timeout` expired, returns `null`. */ - public bzpopmax(keys: string[], timeout: number): T { + public bzpopmax(keys: GlideString[], timeout: number): T { return this.addAndReturn(createBZPopMax(keys, timeout)); } @@ -2273,7 +2278,7 @@ export class BaseTransaction> { * * @param key - The key to return its timeout. * - * Command Response - TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + * Command Response - TTL in milliseconds, `-2` if `key` does not exist, `-1` if `key` exists but has no associated expire. */ public pttl(key: GlideString): T { return this.addAndReturn(createPTTL(key)); @@ -3573,7 +3578,7 @@ export class BaseTransaction> { * If no member could be popped, returns `null`. */ public bzmpop( - keys: string[], + keys: GlideString[], modifier: ScoreFilter, timeout: number, count?: number, @@ -3594,7 +3599,11 @@ export class BaseTransaction> { * * Command Response - The new score of `member`. */ - public zincrby(key: string, increment: number, member: string): T { + public zincrby( + key: GlideString, + increment: number, + member: GlideString, + ): T { return this.addAndReturn(createZIncrBy(key, increment, member)); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 0fb3074630..f93f18e9ce 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4138,7 +4138,7 @@ export function runBaseTests(config: { expect(await client.zadd(key, membersScores)).toEqual(3); expect(await client.zcard(key)).toEqual(3); expect(await client.zrem(key, ["one"])).toEqual(1); - expect(await client.zcard(key)).toEqual(2); + expect(await client.zcard(Buffer.from(key))).toEqual(2); }, protocol); }, config.timeout, @@ -4162,7 +4162,9 @@ export function runBaseTests(config: { expect(await client.zadd(key1, memberScores1)).toEqual(3); expect(await client.zadd(key2, memberScores2)).toEqual(3); - expect(await client.zintercard([key1, key2])).toEqual(2); + expect( + await client.zintercard([key1, Buffer.from(key2)]), + ).toEqual(2); expect(await client.zintercard([key1, nonExistingKey])).toEqual( 0, ); @@ -4218,10 +4220,15 @@ export function runBaseTests(config: { expect(await client.zadd(key2, entries2)).toEqual(1); expect(await client.zadd(key3, entries3)).toEqual(4); - expect(await client.zdiff([key1, key2])).toEqual([ + expect(await client.zdiff([key1, Buffer.from(key2)])).toEqual([ "one", "three", ]); + expect( + await client.zdiff([key1, key2], { + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from("one"), Buffer.from("three")]); expect(await client.zdiff([key1, key3])).toEqual([]); expect(await client.zdiff([nonExistingKey, key3])).toEqual([]); @@ -4231,6 +4238,12 @@ export function runBaseTests(config: { three: 3.0, }; expect(compareMaps(result, expected)).toBe(true); + // same with byte[] + result = await client.zdiffWithScores([ + key1, + Buffer.from(key2), + ]); + expect(compareMaps(result, expected)).toBe(true); result = await client.zdiffWithScores([key1, key3]); expect(compareMaps(result, {})).toBe(true); @@ -4296,7 +4309,11 @@ export function runBaseTests(config: { expect(compareMaps(result1, expected1)).toBe(true); expect( - await client.zdiffstore(key4, [key3, key2, key1]), + await client.zdiffstore(Buffer.from(key4), [ + key3, + key2, + key1, + ]), ).toEqual(1); const result2 = await client.zrangeWithScores(key4, { start: 0, @@ -4304,7 +4321,9 @@ export function runBaseTests(config: { }); expect(compareMaps(result2, { four: 4.0 })).toBe(true); - expect(await client.zdiffstore(key4, [key1, key3])).toEqual(0); + expect( + await client.zdiffstore(key4, [Buffer.from(key1), key3]), + ).toEqual(0); const result3 = await client.zrangeWithScores(key4, { start: 0, stop: -1, @@ -4627,9 +4646,13 @@ export function runBaseTests(config: { ), ).toEqual(2); expect( - await client.zcount(key1, InfBoundary.NegativeInfinity, { - value: 3, - }), + await client.zcount( + Buffer.from(key1), + InfBoundary.NegativeInfinity, + { + value: 3, + }, + ), ).toEqual(3); expect( await client.zcount(key1, InfBoundary.PositiveInfinity, { @@ -4638,7 +4661,7 @@ export function runBaseTests(config: { ).toEqual(0); expect( await client.zcount( - "nonExistingKey", + Buffer.from("nonExistingKey"), InfBoundary.NegativeInfinity, InfBoundary.PositiveInfinity, ), @@ -5156,7 +5179,9 @@ export function runBaseTests(config: { expect(compareMaps(zinterstoreMapMax, expectedMapMax)).toBe(true); // Intersection results are aggregated by the MIN score of elements - expect(await client.zinterstore(key3, [key1, key2], "MIN")).toEqual(2); + expect( + await client.zinterstore(Buffer.from(key3), [key1, key2], "MIN"), + ).toEqual(2); const zinterstoreMapMin = await client.zrangeWithScores(key3, range); const expectedMapMin = { one: 1, @@ -5165,7 +5190,9 @@ export function runBaseTests(config: { expect(compareMaps(zinterstoreMapMin, expectedMapMin)).toBe(true); // Intersection results are aggregated by the SUM score of elements - expect(await client.zinterstore(key3, [key1, key2], "SUM")).toEqual(2); + expect( + await client.zinterstore(key3, [Buffer.from(key1), key2], "SUM"), + ).toEqual(2); const zinterstoreMapSum = await client.zrangeWithScores(key3, range); const expectedMapSum = { one: 3, @@ -5279,9 +5306,19 @@ export function runBaseTests(config: { expect(await client.zadd(key1, membersScores1)).toEqual(2); expect(await client.zadd(key2, membersScores2)).toEqual(3); - const resultZinter = await client.zinter([key1, key2]); - const expectedZinter = ["one", "two"]; - expect(resultZinter).toEqual(expectedZinter); + expect(await client.zinter([key1, key2])).toEqual([ + "one", + "two", + ]); + expect(await client.zinter([key1, Buffer.from(key2)])).toEqual([ + "one", + "two", + ]); + expect( + await client.zinter([key1, key2], { + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from("one"), Buffer.from("two")]); }, protocol); }, config.timeout, @@ -5303,7 +5340,7 @@ export function runBaseTests(config: { const resultZinterWithScores = await client.zinterWithScores([ key1, - key2, + Buffer.from(key2), ]); const expectedZinterWithScores = { one: 2.5, @@ -5334,7 +5371,7 @@ export function runBaseTests(config: { // Intersection results are aggregated by the MAX score of elements const zinterWithScoresResults = await client.zinterWithScores( [key1, key2], - "MAX", + { aggregationType: "MAX" }, ); const expectedMapMax = { one: 1.5, @@ -5363,7 +5400,7 @@ export function runBaseTests(config: { // Intersection results are aggregated by the MIN score of elements const zinterWithScoresResults = await client.zinterWithScores( [key1, key2], - "MIN", + { aggregationType: "MIN" }, ); const expectedMapMin = { one: 1.0, @@ -5392,7 +5429,7 @@ export function runBaseTests(config: { // Intersection results are aggregated by the SUM score of elements const zinterWithScoresResults = await client.zinterWithScores( [key1, key2], - "SUM", + { aggregationType: "SUM" }, ); const expectedMapSum = { one: 2.5, @@ -5424,7 +5461,7 @@ export function runBaseTests(config: { [key1, 3], [key2, 2], ], - "SUM", + { aggregationType: "SUM" }, ); const expectedMapSum = { one: 6, @@ -5987,11 +6024,15 @@ export function runBaseTests(config: { ).toBeNull(); // pops from the second key - expect(await client.bzpopmax([key3, key2], 0.5)).toEqual([ - key2, - "c", - 2.0, - ]); + expect( + await client.bzpopmax([key3, Buffer.from(key2)], 0.5), + ).toEqual([key2, "c", 2.0]); + // pop with decoder + expect( + await client.bzpopmax([key1], 0.5, { + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from(key1), Buffer.from("a"), 1.0]); // key exists but holds non-ZSET value expect(await client.set(key3, "bzpopmax")).toBe("OK"); @@ -6030,11 +6071,15 @@ export function runBaseTests(config: { ).toBeNull(); // pops from the second key - expect(await client.bzpopmin([key3, key2], 0.5)).toEqual([ - key2, - "c", - 2.0, - ]); + expect( + await client.bzpopmin([key3, Buffer.from(key2)], 0.5), + ).toEqual([key2, "c", 2.0]); + // pop with decoder + expect( + await client.bzpopmin([key1], 0.5, { + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from(key1), Buffer.from("b"), 1.5]); // key exists but holds non-ZSET value expect(await client.set(key3, "bzpopmin")).toBe("OK"); @@ -9057,13 +9102,15 @@ export function runBaseTests(config: { expect(await client.zscore(key, member)).toEqual(2.5); // key exists, but value doesn't - expect(await client.zincrby(key, -3.3, othermember)).toEqual( - -3.3, - ); + expect( + await client.zincrby(Buffer.from(key), -3.3, othermember), + ).toEqual(-3.3); expect(await client.zscore(key, othermember)).toEqual(-3.3); // updating existing value in existing key - expect(await client.zincrby(key, 1.0, member)).toEqual(3.5); + expect( + await client.zincrby(key, 1.0, Buffer.from(member)), + ).toEqual(3.5); expect(await client.zscore(key, member)).toEqual(3.5); // Key exists, but it is not a sorted set @@ -9253,7 +9300,14 @@ export function runBaseTests(config: { await client.bzmpop([key1, key2], ScoreFilter.MAX, 0.1), ).toEqual([key1, { b1: 2 }]); expect( - await client.bzmpop([key2, key1], ScoreFilter.MAX, 0.1, 10), + await client.bzmpop( + [key2, Buffer.from(key1)], + ScoreFilter.MAX, + 0.1, + { + count: 10, + }, + ), ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); // ensure that command doesn't time out even if timeout > request timeout (250ms by default) @@ -9265,7 +9319,7 @@ export function runBaseTests(config: { [nonExistingKey], ScoreFilter.MAX, 0.55, - 1, + { count: 1 }, ), ).toBeNull(); @@ -9275,22 +9329,24 @@ export function runBaseTests(config: { client.bzmpop([stringKey], ScoreFilter.MAX, 0.1), ).rejects.toThrow(RequestError); await expect( - client.bzmpop([stringKey], ScoreFilter.MAX, 0.1, 1), + client.bzmpop([stringKey], ScoreFilter.MAX, 0.1, { + count: 1, + }), ).rejects.toThrow(RequestError); // incorrect argument: key list should not be empty await expect( - client.bzmpop([], ScoreFilter.MAX, 0.1, 1), + client.bzmpop([], ScoreFilter.MAX, 0.1, { count: 1 }), ).rejects.toThrow(RequestError); // incorrect argument: count should be greater than 0 await expect( - client.bzmpop([key1], ScoreFilter.MAX, 0.1, 0), + client.bzmpop([key1], ScoreFilter.MAX, 0.1, { count: 0 }), ).rejects.toThrow(RequestError); // incorrect argument: timeout can not be a negative number await expect( - client.bzmpop([key1], ScoreFilter.MAX, -1, 10), + client.bzmpop([key1], ScoreFilter.MAX, -1, { count: 10 }), ).rejects.toThrow(RequestError); // check that order of entries in the response is preserved @@ -9306,7 +9362,7 @@ export function runBaseTests(config: { [key2], ScoreFilter.MIN, 0.1, - 10, + { count: 10 }, ); if (result) { From 8d482bef42104722500d9cbbeb49978b2ad91389 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:17:41 -0700 Subject: [PATCH 234/236] Node: Add Binary support for stream commands, part 1 (#2200) * Node: Add Binary support for stream commands, part 1 Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 79 +++++++++++++++++++----------------- node/src/Commands.ts | 62 ++++++++++++++-------------- node/src/Transaction.ts | 50 +++++++++++------------ node/tests/SharedTests.ts | 85 ++++++++++++++++++++++++++++++--------- 5 files changed, 166 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a1decdad..58ba036b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ * Node: Added XACK commands ([#2112](https://github.com/valkey-io/valkey-glide/pull/2112)) * Node: Added XGROUP SETID command ([#2135]((https://github.com/valkey-io/valkey-glide/pull/2135)) * Node: Added binary variant to string commands ([#2183](https://github.com/valkey-io/valkey-glide/pull/2183)) +* Node: Added binary variant to stream commands ([#2200](https://github.com/valkey-io/valkey-glide/pull/2200)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ce8afe3112..4805e4f02d 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -4731,14 +4731,18 @@ export class BaseClient { * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. * @param options - options detailing how to add to the stream. + * @param options - (Optional) See {@link StreamAddOptions} and {@link DecoderOption}. * @returns The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. */ public async xadd( - key: string, - values: [string, string][], - options?: StreamAddOptions, - ): Promise { - return this.createWritePromise(createXAdd(key, values, options)); + key: GlideString, + values: [GlideString, GlideString][], + options?: StreamAddOptions & DecoderOption, + ): Promise { + return this.createWritePromise( + createXAdd(key, values, options), + options, + ); } /** @@ -4757,7 +4761,7 @@ export class BaseClient { * // Output is 2 since the stream marked 2 entries as deleted. * ``` */ - public async xdel(key: string, ids: string[]): Promise { + public async xdel(key: GlideString, ids: GlideString[]): Promise { return this.createWritePromise(createXDel(key, ids)); } @@ -5024,7 +5028,7 @@ export class BaseClient { * @param consumer - The group consumer. * @param minIdleTime - The minimum idle time for the message to be claimed. * @param ids - An array of entry ids. - * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. + * @param options - (Optional) See {@link StreamClaimOptions} and {@link DecoderOption}. * @returns A `Record` of message entries that are claimed by the consumer. * * @example @@ -5038,13 +5042,14 @@ export class BaseClient { * ``` */ public async xclaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - ids: string[], - options?: StreamClaimOptions, + ids: GlideString[], + options?: StreamClaimOptions & DecoderOption, ): Promise> { + // TODO: convert Record return type to Object array return this.createWritePromise( createXClaim(key, group, consumer, minIdleTime, ids, options), ); @@ -5093,13 +5098,14 @@ export class BaseClient { * ``` */ public async xautoclaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - start: string, + start: GlideString, count?: number, ): Promise<[string, Record, string[]?]> { + // TODO: convert Record return type to Object array return this.createWritePromise( createXAutoClaim(key, group, consumer, minIdleTime, start, count), ); @@ -5218,13 +5224,14 @@ export class BaseClient { * ``` */ public async xgroupCreate( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, options?: StreamGroupOptions, - ): Promise { + ): Promise<"OK"> { return this.createWritePromise( createXGroupCreate(key, groupName, id, options), + { decoder: Decoder.String }, ); } @@ -5244,8 +5251,8 @@ export class BaseClient { * ``` */ public async xgroupDestroy( - key: string, - groupName: string, + key: GlideString, + groupName: GlideString, ): Promise { return this.createWritePromise(createXGroupDestroy(key, groupName)); } @@ -5340,9 +5347,9 @@ export class BaseClient { * ``` */ public async xgroupCreateConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): Promise { return this.createWritePromise( createXGroupCreateConsumer(key, groupName, consumerName), @@ -5366,9 +5373,9 @@ export class BaseClient { * ``` */ public async xgroupDelConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): Promise { return this.createWritePromise( createXGroupDelConsumer(key, groupName, consumerName), @@ -5406,9 +5413,9 @@ export class BaseClient { * ``` */ public async xack( - key: string, - group: string, - ids: string[], + key: GlideString, + group: GlideString, + ids: GlideString[], ): Promise { return this.createWritePromise(createXAck(key, group, ids)); } @@ -5424,7 +5431,6 @@ export class BaseClient { * group. * @param entriesRead - (Optional) A value representing the number of stream entries already read by the group. * This option can only be specified if you are using Valkey version 7.0.0 or above. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. If not set, the default decoder from the client config will be used. * @returns `"OK"`. * * * @example @@ -5433,16 +5439,15 @@ export class BaseClient { * ``` */ public async xgroupSetId( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, entriesRead?: number, - decoder?: Decoder, ): Promise<"OK"> { return this.createWritePromise( createXGroupSetid(key, groupName, id, entriesRead), { - decoder: decoder, + decoder: Decoder.String, }, ); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f3d6d934d1..8a15dd93f0 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2052,7 +2052,7 @@ export type StreamAddOptions = { trim?: StreamTrimOptions; }; -function addTrimOptions(options: StreamTrimOptions, args: string[]) { +function addTrimOptions(options: StreamTrimOptions, args: GlideString[]) { if (options.method === "maxlen") { args.push("MAXLEN"); } else if (options.method === "minid") { @@ -2081,8 +2081,8 @@ function addTrimOptions(options: StreamTrimOptions, args: string[]) { * @internal */ export function createXAdd( - key: string, - values: [string, string][], + key: GlideString, + values: [GlideString, GlideString][], options?: StreamAddOptions, ): command_request.Command { const args = [key]; @@ -2113,8 +2113,8 @@ export function createXAdd( * @internal */ export function createXDel( - key: string, - ids: string[], + key: GlideString, + ids: GlideString[], ): command_request.Command { return createCommand(RequestType.XDel, [key, ...ids]); } @@ -2173,9 +2173,9 @@ export function createXRevRange( * @internal */ export function createXGroupCreateConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): command_request.Command { return createCommand(RequestType.XGroupCreateConsumer, [ key, @@ -2188,9 +2188,9 @@ export function createXGroupCreateConsumer( * @internal */ export function createXGroupDelConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): command_request.Command { return createCommand(RequestType.XGroupDelConsumer, [ key, @@ -2714,11 +2714,11 @@ export type StreamClaimOptions = { /** @internal */ export function createXClaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - ids: string[], + ids: GlideString[], options?: StreamClaimOptions, justId?: boolean, ): command_request.Command { @@ -2740,11 +2740,11 @@ export function createXClaim( /** @internal */ export function createXAutoClaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - start: string, + start: GlideString, count?: number, justId?: boolean, ): command_request.Command { @@ -2784,12 +2784,12 @@ export type StreamGroupOptions = { * @internal */ export function createXGroupCreate( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, options?: StreamGroupOptions, ): command_request.Command { - const args: string[] = [key, groupName, id]; + const args: GlideString[] = [key, groupName, id]; if (options) { if (options.mkStream) { @@ -2809,8 +2809,8 @@ export function createXGroupCreate( * @internal */ export function createXGroupDestroy( - key: string, - groupName: string, + key: GlideString, + groupName: GlideString, ): command_request.Command { return createCommand(RequestType.XGroupDestroy, [key, groupName]); } @@ -3975,9 +3975,9 @@ export function createGetEx( * @internal */ export function createXAck( - key: string, - group: string, - ids: string[], + key: GlideString, + group: GlideString, + ids: GlideString[], ): command_request.Command { return createCommand(RequestType.XAck, [key, group, ...ids]); } @@ -3986,9 +3986,9 @@ export function createXAck( * @internal */ export function createXGroupSetid( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, entriesRead?: number, ): command_request.Command { const args = [key, groupName, id]; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c338d01923..92276ed58c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2504,8 +2504,8 @@ export class BaseTransaction> { * Command Response - The id of the added entry, or `null` if `options.makeStream` is set to `false` and no stream with the matching `key` exists. */ public xadd( - key: string, - values: [string, string][], + key: GlideString, + values: [GlideString, GlideString][], options?: StreamAddOptions, ): T { return this.addAndReturn(createXAdd(key, values, options)); @@ -2522,7 +2522,7 @@ export class BaseTransaction> { * Command Response - The number of entries removed from the stream. This number may be less than the number of entries in * `ids`, if the specified `ids` don't exist in the stream. */ - public xdel(key: string, ids: string[]): T { + public xdel(key: GlideString, ids: GlideString[]): T { return this.addAndReturn(createXDel(key, ids)); } @@ -2760,11 +2760,11 @@ export class BaseTransaction> { * Command Response - A `Record` of message entries that are claimed by the consumer. */ public xclaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - ids: string[], + ids: GlideString[], options?: StreamClaimOptions, ): T { return this.addAndReturn( @@ -2824,11 +2824,11 @@ export class BaseTransaction> { * These IDs are deleted from the Pending Entries List. */ public xautoclaim( - key: string, - group: string, - consumer: string, + key: GlideString, + group: GlideString, + consumer: GlideString, minIdleTime: number, - start: string, + start: GlideString, count?: number, ): T { return this.addAndReturn( @@ -2894,9 +2894,9 @@ export class BaseTransaction> { * Command Response - `"OK"`. */ public xgroupCreate( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, options?: StreamGroupOptions, ): T { return this.addAndReturn( @@ -2914,7 +2914,7 @@ export class BaseTransaction> { * * Command Response - `true` if the consumer group is destroyed. Otherwise, `false`. */ - public xgroupDestroy(key: string, groupName: string): T { + public xgroupDestroy(key: GlideString, groupName: GlideString): T { return this.addAndReturn(createXGroupDestroy(key, groupName)); } @@ -2930,9 +2930,9 @@ export class BaseTransaction> { * Command Response - `true` if the consumer is created. Otherwise, returns `false`. */ public xgroupCreateConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): T { return this.addAndReturn( createXGroupCreateConsumer(key, groupName, consumerName), @@ -2951,9 +2951,9 @@ export class BaseTransaction> { * Command Response - The number of pending messages the `consumer` had before it was deleted. */ public xgroupDelConsumer( - key: string, - groupName: string, - consumerName: string, + key: GlideString, + groupName: GlideString, + consumerName: GlideString, ): T { return this.addAndReturn( createXGroupDelConsumer(key, groupName, consumerName), @@ -2972,7 +2972,7 @@ export class BaseTransaction> { * * Command Response - The number of messages that were successfully acknowledged. */ - public xack(key: string, group: string, ids: string[]): T { + public xack(key: GlideString, group: GlideString, ids: GlideString[]): T { return this.addAndReturn(createXAck(key, group, ids)); } @@ -2990,9 +2990,9 @@ export class BaseTransaction> { * Command Response - `"OK"`. */ public xgroupSetId( - key: string, - groupName: string, - id: string, + key: GlideString, + groupName: GlideString, + id: GlideString, entriesRead?: number, ): T { return this.addAndReturn( diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index f93f18e9ce..94ec63220f 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -6912,15 +6912,24 @@ export function runBaseTests(config: { const group = uuidv4(); const consumer = uuidv4(); - // setup data + // setup data & test binary parameters in XGROUP CREATE commands expect( - await client.xgroupCreate(key1, group, "0", { - mkStream: true, - }), + await client.xgroupCreate( + Buffer.from(key1), + Buffer.from(group), + Buffer.from("0"), + { + mkStream: true, + }, + ), ).toEqual("OK"); expect( - await client.xgroupCreateConsumer(key1, group, consumer), + await client.xgroupCreateConsumer( + Buffer.from(key1), + Buffer.from(group), + Buffer.from(consumer), + ), ).toBeTruthy(); const entry1 = (await client.xadd(key1, [ @@ -9917,9 +9926,11 @@ export function runBaseTests(config: { expect(await client.xdel(key, [streamId1, streamId3])).toEqual( 1, ); - expect(await client.xdel(nonExistentKey, [streamId3])).toEqual( - 0, - ); + expect( + await client.xdel(Buffer.from(nonExistentKey), [ + Buffer.from(streamId3), + ]), + ).toEqual(0); // invalid argument - id list should not be empty await expect(client.xdel(key, [])).rejects.toThrow( @@ -10353,6 +10364,15 @@ export function runBaseTests(config: { "OK", ); + // Testing binary parameters with an non-existing ID + expect( + await client.xgroupSetId( + Buffer.from(key), + Buffer.from(groupName), + Buffer.from("99-99"), + ), + ).toBe("OK"); + // key exists, but is not a stream expect(await client.set(stringKey, "xgroup setid")).toBe("OK"); await expect( @@ -10547,7 +10567,13 @@ export function runBaseTests(config: { // incorrect IDs - response is empty expect( - await client.xclaim(key, group, "consumer", 0, ["000"]), + await client.xclaim( + Buffer.from(key), + Buffer.from(group), + Buffer.from("consumer"), + 0, + [Buffer.from("000")], + ), ).toEqual({}); expect( await client.xclaimJustId(key, group, "consumer", 0, [ @@ -10617,12 +10643,13 @@ export function runBaseTests(config: { }, }); + // testing binary parameters let result = await client.xautoclaim( - key, - group, - "consumer", + Buffer.from(key), + Buffer.from(group), + Buffer.from("consumer"), 0, - "0-0", + Buffer.from("0-0"), 1, ); let expected: typeof result = [ @@ -10757,6 +10784,15 @@ export function runBaseTests(config: { ]), ).toBe(0); + // testing binary parameters + expect( + await client.xack( + Buffer.from(key), + Buffer.from(groupName), + [Buffer.from(stream_id1_0), Buffer.from(stream_id1_1)], + ), + ).toBe(0); + // read the last unacknowledged entry expect( await client.xreadgroup(groupName, consumerName, { @@ -11042,12 +11078,14 @@ export function runBaseTests(config: { ).toEqual(0); // Add two stream entries - const streamid1: string | null = await client.xadd(key, [ + const streamid1: GlideString | null = await client.xadd(key, [ ["field1", "value1"], ]); expect(streamid1).not.toBeNull(); - const streamid2 = await client.xadd(key, [ - ["field2", "value2"], + + // testing binary parameters + const streamid2 = await client.xadd(Buffer.from(key), [ + [Buffer.from("field2"), Buffer.from("value2")], ]); expect(streamid2).not.toBeNull(); @@ -11063,9 +11101,13 @@ export function runBaseTests(config: { }, }); - // delete one of the streams + // delete one of the streams & testing binary parameters expect( - await client.xgroupDelConsumer(key, groupName, consumer), + await client.xgroupDelConsumer( + Buffer.from(key), + Buffer.from(groupName), + Buffer.from(consumer), + ), ).toEqual(2); // attempting to call XGROUP CREATECONSUMER or XGROUP DELCONSUMER with a non-existing key should raise an error @@ -11141,6 +11183,13 @@ export function runBaseTests(config: { expect(await client.xgroupDestroy(key, groupName1)).toEqual( false, ); + // calling again with binary parameters, expecting the same result + expect( + await client.xgroupDestroy( + Buffer.from(key), + Buffer.from(groupName1), + ), + ).toEqual(false); // attempting to destroy a group for a non-existing key should raise an error await expect( From 7e8525b16c8b4b614ddaaf820dbe96eb5b4dd56d Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Fri, 30 Aug 2024 13:47:57 -0700 Subject: [PATCH 235/236] hset parameter updated to HashDataType (#2209) * hset parameter updated to HashDataType Signed-off-by: Prateek Kumar --- node/src/BaseClient.ts | 50 ++++++++++++++++++++++++++++++---- node/src/Commands.ts | 13 +++++++-- node/src/Transaction.ts | 14 +++++++--- node/tests/GlideClient.test.ts | 20 ++++++++++---- node/tests/SharedTests.ts | 35 ++++++++++++++++-------- node/tests/TestUtilities.ts | 2 +- 6 files changed, 103 insertions(+), 31 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 4805e4f02d..0ff1da3d72 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -297,6 +297,28 @@ export type DecoderOption = { decoder?: Decoder; }; +/** + * This function converts an input from HashDataType or Record types to HashDataType. + * + * @param fieldsAndValues - field names and their values. + * @returns HashDataType array containing field names and their values. + */ +export function convertFieldsAndValuesForHset( + fieldsAndValues: HashDataType | Record, +): HashDataType { + let finalFieldAndValues = []; + + if (!Array.isArray(fieldsAndValues)) { + finalFieldAndValues = Object.entries(fieldsAndValues).map((e) => { + return { field: e[0], value: e[1] }; + }); + } else { + finalFieldAndValues = fieldsAndValues; + } + + return finalFieldAndValues; +} + /** * Our purpose in creating PointerResponse type is to mark when response is of number/long pointer response type. * Consequently, when the response is returned, we can check whether it is instanceof the PointerResponse type and pass it to the Rust core function with the proper parameters. @@ -319,6 +341,17 @@ class PointerResponse { } } +/** + * Data type which represents how data are returned from hashes or insterted there. + * Similar to `Record` - see {@link GlideRecord}. + */ +export type HashDataType = { + /** The hash element name. */ + field: GlideString; + /** The hash element value. */ + value: GlideString; +}[]; + /** Represents the credentials for connecting to a server. */ export type RedisCredentials = { /** @@ -1650,22 +1683,27 @@ export class BaseClient { * @see {@link https://valkey.io/commands/hset/|valkey.io} for details. * * @param key - The key of the hash. - * @param fieldValueMap - A field-value map consisting of fields and their corresponding values - * to be set in the hash stored at the specified key. + * @param fieldsAndValues - A list of field names and their values. * @returns The number of fields that were added. * * @example * ```typescript - * // Example usage of the hset method - * const result = await client.hset("my_hash", {"field": "value", "field2": "value2"}); + * // Example usage of the hset method using HashDataType as input type + * const result = await client.hset("my_hash", [{"field": "field1", "value": "value1"}, {"field": "field2", "value": "value2"}]); + * console.log(result); // Output: 2 - Indicates that 2 fields were successfully set in the hash "my_hash". + * + * // Example usage of the hset method using Record as input + * const result = await client.hset("my_hash", {"field1": "value", "field2": "value2"}); * console.log(result); // Output: 2 - Indicates that 2 fields were successfully set in the hash "my_hash". * ``` */ public async hset( key: GlideString, - fieldValueMap: Record, + fieldsAndValues: HashDataType | Record, ): Promise { - return this.createWritePromise(createHSet(key, fieldValueMap)); + return this.createWritePromise( + createHSet(key, convertFieldsAndValuesForHset(fieldsAndValues)), + ); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8a15dd93f0..e19888f629 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -6,7 +6,7 @@ import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { BaseClient } from "src/BaseClient"; +import { BaseClient, HashDataType } from "src/BaseClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import { GlideClient } from "src/GlideClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ @@ -413,11 +413,18 @@ export function createHGet( */ export function createHSet( key: GlideString, - fieldValueMap: Record, + fieldValueList: HashDataType, ): command_request.Command { return createCommand( RequestType.HSet, - [key].concat(Object.entries(fieldValueMap).flat()), + [key].concat( + fieldValueList + .map((fieldValueObject) => [ + fieldValueObject.field, + fieldValueObject.value, + ]) + .flat(), + ), ); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 92276ed58c..1547f52c59 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -5,7 +5,8 @@ import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars GlideString, - ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars + HashDataType, + convertFieldsAndValuesForHset, } from "./BaseClient"; import { @@ -804,13 +805,18 @@ export class BaseTransaction> { * @see {@link https://valkey.io/commands/hset/|valkey.io} for details. * * @param key - The key of the hash. - * @param fieldValueMap - A field-value map consisting of fields and their corresponding values + * @param fieldValueList - A list of field names and their values. * to be set in the hash stored at the specified key. * * Command Response - The number of fields that were added. */ - public hset(key: GlideString, fieldValueMap: Record): T { - return this.addAndReturn(createHSet(key, fieldValueMap)); + public hset( + key: GlideString, + fieldsAndValues: HashDataType | Record, + ): T { + return this.addAndReturn( + createHSet(key, convertFieldsAndValuesForHset(fieldsAndValues)), + ); } /** diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index f18624d261..8efc797900 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from "uuid"; import { Decoder, GlideClient, + HashDataType, ProtocolVersion, RequestError, Transaction, @@ -1166,11 +1167,12 @@ describe("GlideClient", () => { const ages = ["30", "25", "35", "20", "40"]; for (let i = 0; i < ages.length; i++) { + const fieldValueList: HashDataType = [ + { field: "name", value: names[i] }, + { field: "age", value: ages[i] }, + ]; expect( - await client.hset(setPrefix + (i + 1), { - name: names[i], - age: ages[i], - }), + await client.hset(setPrefix + (i + 1), fieldValueList), ).toEqual(2); } @@ -1331,8 +1333,14 @@ describe("GlideClient", () => { // transaction test const transaction = new Transaction() - .hset(hashPrefix + 1, { name: "Alice", age: "30" }) - .hset(hashPrefix + 2, { name: "Bob", age: "25" }) + .hset(hashPrefix + 1, [ + { field: "name", value: "Alice" }, + { field: "age", value: "30" }, + ]) + .hset(hashPrefix + 2, { + name: "Bob", + age: "25", + }) .del([list]) .lpush(list, ["2", "1"]) .sort(list, { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 94ec63220f..aa3ecb2600 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -8,6 +8,7 @@ // represents a running server instance. See first 2 test cases as examples. import { expect, it } from "@jest/globals"; +import { HashDataType } from "src/BaseClient"; import { v4 as uuidv4 } from "uuid"; import { BaseClientConfiguration, @@ -1332,13 +1333,20 @@ export function runBaseTests(config: { const field1 = uuidv4(); const field2 = uuidv4(); const value = uuidv4(); - const fieldValueMap = { - [field1]: value, - [field2]: value, - }; + const fieldValueList: HashDataType = [ + { + field: Buffer.from(field1), + value: Buffer.from(value), + }, + { + field: Buffer.from(field2), + value: Buffer.from(value), + }, + ]; + const valueEncoded = Buffer.from(value); - expect(await client.hset(key, fieldValueMap)).toEqual(2); + expect(await client.hset(key, fieldValueList)).toEqual(2); expect( await client.hget(Buffer.from(key), Buffer.from(field1)), ).toEqual(value); @@ -1864,12 +1872,18 @@ export function runBaseTests(config: { const key1 = uuidv4(); const field1 = uuidv4(); const field2 = uuidv4(); - const fieldValueMap = { - [field1]: "value1", - [field2]: "value2", - }; + const fieldValueList = [ + { + field: field1, + value: "value1", + }, + { + field: field2, + value: "value2", + }, + ]; - expect(await client.hset(key1, fieldValueMap)).toEqual(2); + expect(await client.hset(key1, fieldValueList)).toEqual(2); expect(await client.hlen(key1)).toEqual(2); expect(await client.hdel(key1, [field1])).toEqual(1); expect(await client.hlen(Buffer.from(key1))).toEqual(1); @@ -1891,7 +1905,6 @@ export function runBaseTests(config: { [field1]: "value1", [field2]: "value2", }; - const value1Encoded = Buffer.from("value1"); const value2Encoded = Buffer.from("value2"); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ba8e185232..7136c043ab 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -762,7 +762,7 @@ export async function transactionTest( responseData.push(["append(key1, value)", 3]); baseTransaction.del([key1]); responseData.push(["del([key1])", 1]); - baseTransaction.hset(key4, { [field]: value }); + baseTransaction.hset(key4, [{ field, value }]); responseData.push(["hset(key4, { [field]: value })", 1]); baseTransaction.hscan(key4, "0"); responseData.push(['hscan(key4, "0")', ["0", [field, value]]]); From 1f7e55fe3eed84dd91f396b4fced8d007e37b33e Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Fri, 30 Aug 2024 16:35:31 -0700 Subject: [PATCH 236/236] Decoder Options added for hget (#2215) Signed-off-by: Prateek Kumar --- node/src/BaseClient.ts | 9 +++------ node/tests/SharedTests.ts | 16 +++++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0ff1da3d72..2f1be67495 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1649,8 +1649,7 @@ export class BaseClient { * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. - * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. + * @param options - (Optional) See {@link DecoderOption}. * @returns the value associated with `field`, or null when `field` is not present in the hash or `key` does not exist. * * @example @@ -1671,11 +1670,9 @@ export class BaseClient { public async hget( key: GlideString, field: GlideString, - decoder?: Decoder, + options?: DecoderOption, ): Promise { - return this.createWritePromise(createHGet(key, field), { - decoder: decoder, - }); + return this.createWritePromise(createHGet(key, field), options); } /** Sets the specified fields to their respective values in the hash stored at `key`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index aa3ecb2600..25a44b5f92 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1356,14 +1356,16 @@ export function runBaseTests(config: { ); //hget with binary buffer - expect(await client.hget(key, field1, Decoder.Bytes)).toEqual( - valueEncoded, - ); - expect(await client.hget(key, field2, Decoder.Bytes)).toEqual( - valueEncoded, - ); expect( - await client.hget(key, "nonExistingField", Decoder.Bytes), + await client.hget(key, field1, { decoder: Decoder.Bytes }), + ).toEqual(valueEncoded); + expect( + await client.hget(key, field2, { decoder: Decoder.Bytes }), + ).toEqual(valueEncoded); + expect( + await client.hget(key, "nonExistingField", { + decoder: Decoder.Bytes, + }), ).toEqual(null); }, protocol); },