From ff8319d2d7fe7afb6ae1eb129ff5526370aef4af Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:39:32 +0300 Subject: [PATCH 01/13] fix(pool): chain promise handlers to prevent unhandled rejections (#3035) --- packages/client/lib/client/pool.spec.ts | 31 +++++++++++++++++++++++++ packages/client/lib/client/pool.ts | 5 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/client/pool.spec.ts b/packages/client/lib/client/pool.spec.ts index 8fc7a258df9..f292dc171c7 100644 --- a/packages/client/lib/client/pool.spec.ts +++ b/packages/client/lib/client/pool.spec.ts @@ -8,4 +8,35 @@ describe('RedisClientPool', () => { 'PONG' ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientPool( + 'proper error propagation in sequential operations', + async (pool) => { + let hasUnhandledRejection = false; + + process.once('unhandledRejection', () => { + hasUnhandledRejection = true; + }); + + const groupName = 'test-group'; + const streamName = 'test-stream'; + + // First attempt - should succeed + await pool.xGroupCreate(streamName, groupName, '0', { + MKSTREAM: true, + }); + + // Subsequent attempts - should all throw BUSYGROUP errors and be handled properly + for (let i = 0; i < 3; i++) { + await assert.rejects( + pool.xGroupCreate(streamName, groupName, '0', { + MKSTREAM: true, + }) + ); + } + + assert.equal(hasUnhandledRejection, false); + }, + GLOBAL.SERVERS.OPEN + ); }); diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index 6f633c9caa7..b53bb2c7e61 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -438,8 +438,9 @@ export class RedisClientPool< ) { const result = fn(node.value); if (result instanceof Promise) { - result.then(resolve, reject); - result.finally(() => this.#returnClient(node)) + result + .then(resolve, reject) + .finally(() => this.#returnClient(node)) } else { resolve(result); this.#returnClient(node); From d941ec5a4cc73ff8d0965becfdca41a4dc521004 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:58:28 +0300 Subject: [PATCH 02/13] Add Redis 8.2 New Stream Commands (#3029) * chore: update Redis version from 8.2-RC1-pre to 8.2-rc1 * feat: implement XDELEX command for Redis 8.2 * feat: implement XACKDEL command for Redis 8.2 * refactor: create shared stream deletion types for Redis 8.2 commands * feat: add Redis 8.2 deletion policies to XTRIM command * feat: add Redis 8.2 deletion policies to XADD commands * fix: correct XDELEX command method name and test parameter --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/commands/XACKDEL.spec.ts | 196 ++++++++++++++++++ packages/client/lib/commands/XACKDEL.ts | 45 ++++ packages/client/lib/commands/XADD.spec.ts | 80 +++++++ packages/client/lib/commands/XADD.ts | 8 + .../lib/commands/XADD_NOMKSTREAM.spec.ts | 88 +++++++- packages/client/lib/commands/XDELEX.spec.ts | 156 ++++++++++++++ packages/client/lib/commands/XDELEX.ts | 42 ++++ packages/client/lib/commands/XTRIM.spec.ts | 95 ++++++++- packages/client/lib/commands/XTRIM.ts | 8 + .../lib/commands/common-stream.types.ts | 28 +++ packages/client/lib/commands/index.ts | 6 + packages/client/lib/sentinel/test-util.ts | 2 +- packages/client/lib/test-utils.ts | 2 +- packages/entraid/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/search/lib/test-utils.ts | 2 +- packages/time-series/lib/test-utils.ts | 2 +- 19 files changed, 746 insertions(+), 22 deletions(-) create mode 100644 packages/client/lib/commands/XACKDEL.spec.ts create mode 100644 packages/client/lib/commands/XACKDEL.ts create mode 100644 packages/client/lib/commands/XDELEX.spec.ts create mode 100644 packages/client/lib/commands/XDELEX.ts create mode 100644 packages/client/lib/commands/common-stream.types.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89efdb61114..df8cb1d1b6c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: ["18", "20", "22"] - redis-version: ["rs-7.2.0-v13", "rs-7.4.0-v1", "8.0.2", "8.2-M01-pre"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2-rc1"] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 4396c94f726..268ebca8cb9 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisBloomModules from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/client/lib/commands/XACKDEL.spec.ts b/packages/client/lib/commands/XACKDEL.spec.ts new file mode 100644 index 00000000000..9d7bad15a24 --- /dev/null +++ b/packages/client/lib/commands/XACKDEL.spec.ts @@ -0,0 +1,196 @@ +import { strict as assert } from "node:assert"; +import XACKDEL from "./XACKDEL"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; +import { + STREAM_DELETION_POLICY, + STREAM_DELETION_REPLY_CODES, +} from "./common-stream.types"; + +describe("XACKDEL", () => { + describe("transformArguments", () => { + it("string - without policy", () => { + assert.deepEqual(parseArgs(XACKDEL, "key", "group", "0-0"), [ + "XACKDEL", + "key", + "group", + "IDS", + "1", + "0-0", + ]); + }); + + it("string - with policy", () => { + assert.deepEqual( + parseArgs( + XACKDEL, + "key", + "group", + "0-0", + STREAM_DELETION_POLICY.KEEPREF + ), + ["XACKDEL", "key", "group", "KEEPREF", "IDS", "1", "0-0"] + ); + }); + + it("array - without policy", () => { + assert.deepEqual(parseArgs(XACKDEL, "key", "group", ["0-0", "1-0"]), [ + "XACKDEL", + "key", + "group", + "IDS", + "2", + "0-0", + "1-0", + ]); + }); + + it("array - with policy", () => { + assert.deepEqual( + parseArgs( + XACKDEL, + "key", + "group", + ["0-0", "1-0"], + STREAM_DELETION_POLICY.DELREF + ), + ["XACKDEL", "key", "group", "DELREF", "IDS", "2", "0-0", "1-0"] + ); + }); + }); + + testUtils.testAll( + `XACKDEL non-existing key - without policy`, + async (client) => { + const reply = await client.xAckDel("{tag}stream-key", "testgroup", "0-0"); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL existing key - without policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer group, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel(streamKey, groupName, messageId); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL existing key - with policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer group, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + messageId, + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL acknowledge policy - with consumer group`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer groups, stream and message + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + await client.xGroupCreate(streamKey, "some-other-group", "0"); + const messageId = await client.xAdd(streamKey, "*", { field: "value" }); + + // read message + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + messageId, + STREAM_DELETION_POLICY.ACKED + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XACKDEL multiple keys`, + async (client) => { + const streamKey = "{tag}stream-key"; + const groupName = "testgroup"; + + // create consumer groups, stream and add messages + await client.xGroupCreate(streamKey, groupName, "0", { MKSTREAM: true }); + const messageIds = await Promise.all([ + client.xAdd(streamKey, "*", { field: "value1" }), + client.xAdd(streamKey, "*", { field: "value2" }), + ]); + + // read messages + await client.xReadGroup(groupName, "testconsumer", { + key: streamKey, + id: ">", + }); + + const reply = await client.xAckDel( + streamKey, + groupName, + [...messageIds, "0-0"], + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [ + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.NOT_FOUND, + ]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); +}); diff --git a/packages/client/lib/commands/XACKDEL.ts b/packages/client/lib/commands/XACKDEL.ts new file mode 100644 index 00000000000..6e209879e49 --- /dev/null +++ b/packages/client/lib/commands/XACKDEL.ts @@ -0,0 +1,45 @@ +import { CommandParser } from "../client/parser"; +import { RedisArgument, ArrayReply, Command } from "../RESP/types"; +import { + StreamDeletionReplyCode, + StreamDeletionPolicy, +} from "./common-stream.types"; +import { RedisVariadicArgument } from "./generic-transformers"; + +/** + * Acknowledges and deletes one or multiple messages for a stream consumer group + */ +export default { + IS_READ_ONLY: false, + /** + * Constructs the XACKDEL command to acknowledge and delete one or multiple messages for a stream consumer group + * + * @param parser - The command parser + * @param key - The stream key + * @param group - The consumer group name + * @param id - One or more message IDs to acknowledge and delete + * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) + * @returns Array of integers: -1 (not found), 1 (acknowledged and deleted), 2 (acknowledged with dangling refs) + * @see https://redis.io/commands/xackdel/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + group: RedisArgument, + id: RedisVariadicArgument, + policy?: StreamDeletionPolicy + ) { + parser.push("XACKDEL"); + parser.pushKey(key); + parser.push(group); + + if (policy) { + parser.push(policy); + } + + parser.push("IDS"); + parser.pushVariadicWithLength(id); + }, + transformReply: + undefined as unknown as () => ArrayReply, +} as const satisfies Command; diff --git a/packages/client/lib/commands/XADD.spec.ts b/packages/client/lib/commands/XADD.spec.ts index 321581d0865..a41e8682751 100644 --- a/packages/client/lib/commands/XADD.spec.ts +++ b/packages/client/lib/commands/XADD.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XADD from './XADD'; import { parseArgs } from './generic-transformers'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XADD', () => { describe('transformArguments', () => { @@ -78,6 +79,37 @@ describe('XADD', () => { ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value'] ); }); + + it('with TRIM.policy', () => { + assert.deepEqual( + parseArgs(XADD, 'key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + policy: STREAM_DELETION_POLICY.DELREF + } + }), + ['XADD', 'key', '1000', 'DELREF', '*', 'field', 'value'] + ); + }); + + it('with all TRIM options', () => { + assert.deepEqual( + parseArgs(XADD, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 100, + policy: STREAM_DELETION_POLICY.ACKED + } + }), + ['XADD', 'key', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value'] + ); + }); }); testUtils.testAll('xAdd', async client => { @@ -91,4 +123,52 @@ describe('XADD', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testAll( + 'xAdd with TRIM policy', + async (client) => { + assert.equal( + typeof await client.xAdd('{tag}key', '*', + { field: 'value' }, + { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000, + policy: STREAM_DELETION_POLICY.KEEPREF + } + } + ), + 'string' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + 'xAdd with all TRIM options', + async (client) => { + assert.equal( + typeof await client.xAdd('{tag}key2', '*', + { field: 'value' }, + { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 10, + policy: STREAM_DELETION_POLICY.DELREF + } + } + ), + 'string' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index b0c50b1bfdb..f2509a9fa7b 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -1,5 +1,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { StreamDeletionPolicy } from './common-stream.types'; import { Tail } from './generic-transformers'; /** @@ -10,6 +11,7 @@ import { Tail } from './generic-transformers'; * @property TRIM.strategyModifier - Exact ('=') or approximate ('~') trimming * @property TRIM.threshold - Maximum stream length or minimum ID to retain * @property TRIM.limit - Maximum number of entries to trim in one call + * @property TRIM.policy - Policy to apply when trimming entries (optional, defaults to KEEPREF) */ export interface XAddOptions { TRIM?: { @@ -17,6 +19,8 @@ export interface XAddOptions { strategyModifier?: '=' | '~'; threshold: number; limit?: number; + /** added in 8.2 */ + policy?: StreamDeletionPolicy; }; } @@ -58,6 +62,10 @@ export function parseXAddArguments( if (options.TRIM.limit) { parser.push('LIMIT', options.TRIM.limit.toString()); } + + if (options.TRIM.policy) { + parser.push(options.TRIM.policy); + } } parser.push(id); diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts index 97927f212ff..a957d0f06c1 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; import { parseArgs } from './generic-transformers'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XADD NOMKSTREAM', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -80,17 +81,82 @@ describe('XADD NOMKSTREAM', () => { ['XADD', 'key', 'NOMKSTREAM', '1000', 'LIMIT', '1', '*', 'field', 'value'] ); }); - }); - testUtils.testAll('xAddNoMkStream', async client => { - assert.equal( - await client.xAddNoMkStream('key', '*', { - field: 'value' - }), - null - ); - }, { - client: GLOBAL.SERVERS.OPEN, - cluster: GLOBAL.CLUSTERS.OPEN + it('with TRIM.policy', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + policy: STREAM_DELETION_POLICY.DELREF + } + }), + ['XADD', 'key', 'NOMKSTREAM', '1000', 'DELREF', '*', 'field', 'value'] + ); + }); + + it('with all TRIM options', () => { + assert.deepEqual( + parseArgs(XADD_NOMKSTREAM, 'key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '~', + threshold: 1000, + limit: 100, + policy: STREAM_DELETION_POLICY.ACKED + } + }), + ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '~', '1000', 'LIMIT', '100', 'ACKED', '*', 'field', 'value'] + ); + }); }); + + testUtils.testAll( + 'xAddNoMkStream - null when stream does not exist', + async (client) => { + assert.equal( + await client.xAddNoMkStream('{tag}nonexistent-stream', '*', { + field: 'value' + }), + null + ); + }, + { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + } + ); + + testUtils.testAll( + 'xAddNoMkStream - with all TRIM options', + async (client) => { + const streamKey = '{tag}stream'; + + // Create stream and add some messages + await client.xAdd(streamKey, '*', { field: 'value1' }); + + // Use NOMKSTREAM with all TRIM options + const messageId = await client.xAddNoMkStream(streamKey, '*', + { field: 'value2' }, + { + TRIM: { + strategyModifier: '~', + limit: 1, + strategy: 'MAXLEN', + threshold: 2, + policy: STREAM_DELETION_POLICY.DELREF + } + } + ); + + assert.equal(typeof messageId, 'string'); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XDELEX.spec.ts b/packages/client/lib/commands/XDELEX.spec.ts new file mode 100644 index 00000000000..8c421503256 --- /dev/null +++ b/packages/client/lib/commands/XDELEX.spec.ts @@ -0,0 +1,156 @@ +import { strict as assert } from "node:assert"; +import XDELEX from "./XDELEX"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; +import { + STREAM_DELETION_POLICY, + STREAM_DELETION_REPLY_CODES, +} from "./common-stream.types"; + +describe("XDELEX", () => { + describe("transformArguments", () => { + it("string - without policy", () => { + assert.deepEqual(parseArgs(XDELEX, "key", "0-0"), [ + "XDELEX", + "key", + "IDS", + "1", + "0-0", + ]); + }); + + it("string - with policy", () => { + assert.deepEqual( + parseArgs(XDELEX, "key", "0-0", STREAM_DELETION_POLICY.KEEPREF), + ["XDELEX", "key", "KEEPREF", "IDS", "1", "0-0"] + ); + }); + + it("array - without policy", () => { + assert.deepEqual(parseArgs(XDELEX, "key", ["0-0", "1-0"]), [ + "XDELEX", + "key", + "IDS", + "2", + "0-0", + "1-0", + ]); + }); + + it("array - with policy", () => { + assert.deepEqual( + parseArgs(XDELEX, "key", ["0-0", "1-0"], STREAM_DELETION_POLICY.DELREF), + ["XDELEX", "key", "DELREF", "IDS", "2", "0-0", "1-0"] + ); + }); + }); + + testUtils.testAll( + `XDELEX non-existing key - without policy`, + async (client) => { + const reply = await client.xDelEx("{tag}stream-key", "0-0"); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.NOT_FOUND]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX existing key - without policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + const reply = await client.xDelEx( + streamKey, + messageId, + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX existing key - with policy`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + const reply = await client.xDelEx( + streamKey, + messageId, + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DELETED]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX acknowledge policy - with consumer group`, + async (client) => { + const streamKey = "{tag}stream-key"; + + // Add a message to the stream + const messageId = await client.xAdd(streamKey, "*", { + field: "value", + }); + + // Create consumer group + await client.xGroupCreate(streamKey, "testgroup", "0"); + + const reply = await client.xDelEx( + streamKey, + messageId, + STREAM_DELETION_POLICY.ACKED + ); + assert.deepEqual(reply, [STREAM_DELETION_REPLY_CODES.DANGLING_REFS]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + `XDELEX multiple keys`, + async (client) => { + const streamKey = "{tag}stream-key"; + const messageIds = await Promise.all([ + client.xAdd(streamKey, "*", { + field: "value1", + }), + client.xAdd(streamKey, "*", { + field: "value2", + }), + ]); + + const reply = await client.xDelEx( + streamKey, + [...messageIds, "0-0"], + STREAM_DELETION_POLICY.DELREF + ); + assert.deepEqual(reply, [ + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.DELETED, + STREAM_DELETION_REPLY_CODES.NOT_FOUND, + ]); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); +}); diff --git a/packages/client/lib/commands/XDELEX.ts b/packages/client/lib/commands/XDELEX.ts new file mode 100644 index 00000000000..021dd0a9e13 --- /dev/null +++ b/packages/client/lib/commands/XDELEX.ts @@ -0,0 +1,42 @@ +import { CommandParser } from "../client/parser"; +import { RedisArgument, ArrayReply, Command } from "../RESP/types"; +import { + StreamDeletionPolicy, + StreamDeletionReplyCode, +} from "./common-stream.types"; +import { RedisVariadicArgument } from "./generic-transformers"; + +/** + * Deletes one or multiple entries from the stream + */ +export default { + IS_READ_ONLY: false, + /** + * Constructs the XDELEX command to delete one or multiple entries from the stream + * + * @param parser - The command parser + * @param key - The stream key + * @param id - One or more message IDs to delete + * @param policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) + * @returns Array of integers: -1 (not found), 1 (deleted), 2 (dangling refs) + * @see https://redis.io/commands/xdelex/ + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + id: RedisVariadicArgument, + policy?: StreamDeletionPolicy + ) { + parser.push("XDELEX"); + parser.pushKey(key); + + if (policy) { + parser.push(policy); + } + + parser.push("IDS"); + parser.pushVariadicWithLength(id); + }, + transformReply: + undefined as unknown as () => ArrayReply, +} as const satisfies Command; diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index 2c31f0fef92..b88cf84676e 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XTRIM from './XTRIM'; import { parseArgs } from './generic-transformers'; +import { STREAM_DELETION_POLICY } from './common-stream.types'; describe('XTRIM', () => { describe('transformArguments', () => { @@ -12,6 +13,13 @@ describe('XTRIM', () => { ); }); + it('simple - MINID', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MINID', 123), + ['XTRIM', 'key', 'MINID', '123'] + ); + }); + it('with strategyModifier', () => { assert.deepEqual( parseArgs(XTRIM, 'key', 'MAXLEN', 1, { @@ -39,15 +47,96 @@ describe('XTRIM', () => { ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1'] ); }); + + it('with policy', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { + policy: STREAM_DELETION_POLICY.DELREF + }), + ['XTRIM', 'key', 'MAXLEN', '1', 'DELREF'] + ); + }); + + it('with all options', () => { + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { + strategyModifier: '~', + LIMIT: 100, + policy: STREAM_DELETION_POLICY.ACKED + }), + ['XTRIM', 'key', 'MAXLEN', '~', '1', 'LIMIT', '100', 'ACKED'] + ); + }); + }); + + testUtils.testAll('xTrim with MAXLEN', async client => { + assert.equal( + typeof await client.xTrim('key', 'MAXLEN', 1), + 'number' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, }); - testUtils.testAll('xTrim', async client => { + testUtils.testAll('xTrim with MINID', async client => { assert.equal( - await client.xTrim('key', 'MAXLEN', 1), - 0 + typeof await client.xTrim('key', 'MINID', 1), + 'number' ); }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN, }); + + testUtils.testAll( + 'xTrim with LIMIT', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MAXLEN', 1000, { + strategyModifier: '~', + LIMIT: 10 + }), + 'number' + ); + }, + { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + } + ); + + testUtils.testAll( + 'xTrim with policy', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MAXLEN', 0, { + policy: STREAM_DELETION_POLICY.DELREF + }), + 'number' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); + + testUtils.testAll( + 'xTrim with all options', + async (client) => { + assert.equal( + typeof await client.xTrim('{tag}key', 'MINID', 0, { + strategyModifier: '~', + LIMIT: 10, + policy: STREAM_DELETION_POLICY.KEEPREF + }), + 'number' + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 2] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 2] }, + } + ); }); diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 6125720111a..34171d4611e 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,16 +1,20 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { StreamDeletionPolicy } from './common-stream.types'; /** * Options for the XTRIM command * * @property strategyModifier - Exact ('=') or approximate ('~') trimming * @property LIMIT - Maximum number of entries to trim in one call (Redis 6.2+) + * @property policy - Policy to apply when deleting entries (optional, defaults to KEEPREF) */ export interface XTrimOptions { strategyModifier?: '=' | '~'; /** added in 6.2 */ LIMIT?: number; + /** added in 8.2 */ + policy?: StreamDeletionPolicy; } /** @@ -49,6 +53,10 @@ export default { if (options?.LIMIT) { parser.push('LIMIT', options.LIMIT.toString()); } + + if (options?.policy) { + parser.push(options.policy); + } }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/common-stream.types.ts b/packages/client/lib/commands/common-stream.types.ts new file mode 100644 index 00000000000..60955b6e3c3 --- /dev/null +++ b/packages/client/lib/commands/common-stream.types.ts @@ -0,0 +1,28 @@ +/** Common stream deletion policies + * + * Added in Redis 8.2 + */ +export const STREAM_DELETION_POLICY = { + /** Preserve references (default) */ + KEEPREF: "KEEPREF", + /** Delete all references */ + DELREF: "DELREF", + /** Only acknowledged entries */ + ACKED: "ACKED", +} as const; + +export type StreamDeletionPolicy = + (typeof STREAM_DELETION_POLICY)[keyof typeof STREAM_DELETION_POLICY]; + +/** Common reply codes for stream deletion operations */ +export const STREAM_DELETION_REPLY_CODES = { + /** ID not found */ + NOT_FOUND: -1, + /** Entry deleted */ + DELETED: 1, + /** Dangling references */ + DANGLING_REFS: 2, +} as const; + +export type StreamDeletionReplyCode = + (typeof STREAM_DELETION_REPLY_CODES)[keyof typeof STREAM_DELETION_REPLY_CODES]; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 87ab8d10b8f..4614c8b282b 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -280,6 +280,7 @@ import TYPE from './TYPE'; import UNLINK from './UNLINK'; import WAIT from './WAIT'; import XACK from './XACK'; +import XACKDEL from './XACKDEL'; import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; import XADD from './XADD'; import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; @@ -287,6 +288,7 @@ import XAUTOCLAIM from './XAUTOCLAIM'; import XCLAIM_JUSTID from './XCLAIM_JUSTID'; import XCLAIM from './XCLAIM'; import XDEL from './XDEL'; +import XDELEX from './XDELEX'; import XGROUP_CREATE from './XGROUP_CREATE'; import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; @@ -924,6 +926,8 @@ export default { wait: WAIT, XACK, xAck: XACK, + XACKDEL, + xAckDel: XACKDEL, XADD_NOMKSTREAM, xAddNoMkStream: XADD_NOMKSTREAM, XADD, @@ -938,6 +942,8 @@ export default { xClaim: XCLAIM, XDEL, xDel: XDEL, + XDELEX, + xDelEx: XDELEX, XGROUP_CREATE, xGroupCreate: XGROUP_CREATE, XGROUP_CREATECONSUMER, diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index c8efa47f41d..7c4752a8852 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index b9b906e943c..86b6ed294ac 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 3c561d4ba44..e5d977a6b40 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -6,7 +6,7 @@ import { EntraidCredentialsProvider } from './entraid-credentials-provider'; export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 6b6859d61bf..cba2d95e737 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index a2b9c816da1..d4d91307b99 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -5,7 +5,7 @@ import { RespVersions } from '@redis/client'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 8a664ee8df2..9c59918e705 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2-rc1' }); export const GLOBAL = { From c2dc73c5d8eb1009d23645eb9f5fed4c59465711 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:00:46 +0100 Subject: [PATCH 03/13] docs: DOC-5473 added time series doc examples (#3030) --- doctests/dt-time-series.js | 635 +++++++++++++++++++++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 doctests/dt-time-series.js diff --git a/doctests/dt-time-series.js b/doctests/dt-time-series.js new file mode 100644 index 00000000000..d2d94e7dc40 --- /dev/null +++ b/doctests/dt-time-series.js @@ -0,0 +1,635 @@ +// EXAMPLE: time_series_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; +import { TIME_SERIES_AGGREGATION_TYPE, TIME_SERIES_REDUCERS } from '@redis/time-series'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del([ + 'thermometer:1', 'thermometer:2', 'thermometer:3', + 'rg:1', 'rg:2', 'rg:3', 'rg:4', + 'sensor3', + 'wind:1', 'wind:2', 'wind:3', 'wind:4', + 'hyg:1', 'hyg:compacted' +]); +// REMOVE_END + +// STEP_START create +const res1 = await client.ts.create('thermometer:1'); +console.log(res1); // >>> OK + +const res2 = await client.type('thermometer:1'); +console.log(res2); // >>> TSDB-TYPE + +const res3 = await client.ts.info('thermometer:1'); +console.log(res3); +// >>> { rules: [], ... totalSamples: 0, ... +// STEP_END +// REMOVE_START +assert.equal(res1, 'OK'); +assert.equal(res2, 'TSDB-TYPE'); +assert.equal(res3.totalSamples, 0); +// REMOVE_END + +// STEP_START create_retention +const res4 = await client.ts.add('thermometer:2', 1, 10.8, { RETENTION: 100 }); +console.log(res4); // >>> 1 + +const res5 = await client.ts.info('thermometer:2'); +console.log(res5); +// >>> { rules: [], ... retentionTime: 100, ... +// STEP_END +// REMOVE_START +assert.equal(res4, 1); +assert.equal(res5.retentionTime, 100); +// REMOVE_END + +// STEP_START create_labels +const res6 = await client.ts.add('thermometer:3', 1, 10.4, { + LABELS: { location: 'UK', type: 'Mercury' } +}); +console.log(res6); // >>> 1 + +const res7 = await client.ts.info('thermometer:3'); +console.log(res7); +// >>> { labels: [{ name: 'location', value: 'UK' }, { name: 'type', value: 'Mercury' }], ... } +// STEP_END +// REMOVE_START +assert.equal(res6, 1); +assert.deepEqual(res7.labels, [ + { name: 'location', value: 'UK' }, + { name: 'type', value: 'Mercury' }, +]); +// REMOVE_END + +// STEP_START madd +const res8 = await client.ts.mAdd([ + { key: 'thermometer:1', timestamp: 1, value: 9.2 }, + { key: 'thermometer:1', timestamp: 2, value: 9.9 }, + { key: 'thermometer:2', timestamp: 2, value: 10.3 } +]); +console.log(res8); // >>> [1, 2, 2] +// STEP_END +// REMOVE_START +assert.deepEqual(res8, [1, 2, 2]); +// REMOVE_END + +// STEP_START get +// The last recorded temperature for thermometer:2 +// was 10.3 at time 2. +const res9 = await client.ts.get('thermometer:2'); +console.log(res9); // >>> { timestamp: 2, value: 10.3 } +// STEP_END +// REMOVE_START +assert.equal(res9.timestamp, 2); +assert.equal(res9.value, 10.3); +// REMOVE_END + +// STEP_START range +// Add 5 data points to a time series named "rg:1". +const res10 = await client.ts.create('rg:1'); +console.log(res10); // >>> OK + +const res11 = await client.ts.mAdd([ + { key: 'rg:1', timestamp: 0, value: 18 }, + { key: 'rg:1', timestamp: 1, value: 14 }, + { key: 'rg:1', timestamp: 2, value: 22 }, + { key: 'rg:1', timestamp: 3, value: 18 }, + { key: 'rg:1', timestamp: 4, value: 24 } +]); +console.log(res11); // >>> [0, 1, 2, 3, 4] + +// Retrieve all the data points in ascending order. +const res12 = await client.ts.range('rg:1', '-', '+'); +console.log(res12); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }, ...] + +// Retrieve data points up to time 1 (inclusive). +const res13 = await client.ts.range('rg:1', '-', 1); +console.log(res13); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }] + +// Retrieve data points from time 3 onwards. +const res14 = await client.ts.range('rg:1', 3, '+'); +console.log(res14); +// >>> [{ timestamp: 3, value: 18 }, { timestamp: 4, value: 24 }] + +// Retrieve all the data points in descending order. +const res15 = await client.ts.revRange('rg:1', '-', '+'); +console.log(res15); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 3, value: 18 }, ...] + +// Retrieve data points up to time 1 (inclusive), but return them +// in descending order. +const res16 = await client.ts.revRange('rg:1', '-', 1); +console.log(res16); +// >>> [{ timestamp: 1, value: 14 }, { timestamp: 0, value: 18 }] +// STEP_END +// REMOVE_START +assert.equal(res10, 'OK'); +assert.deepEqual(res11, [0, 1, 2, 3, 4]); + +assert.deepEqual(res12, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 }, + { timestamp: 2, value: 22 }, + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res13, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 } +]); +assert.deepEqual(res14, [ + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res15, [ + { timestamp: 4, value: 24 }, + { timestamp: 3, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +assert.deepEqual(res16, [ + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +// REMOVE_END + +// STEP_START range_filter +const res17 = await client.ts.range('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4] +}); +console.log(res17); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 2, value: 22 }, { timestamp: 4, value: 24 }] + +const res18 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 20, max: 25 } +}); +console.log(res18); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 2, value: 22 }] + +const res19 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 22, max: 22 }, + COUNT: 1 +}); +console.log(res19); +// >>> [{ timestamp: 2, value: 22 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res17, [ + { timestamp: 0, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res18, [ + { timestamp: 4, value: 24 }, + { timestamp: 2, value: 22 } +]); +assert.deepEqual(res19, [ + { timestamp: 2, value: 22 } +]); +// REMOVE_END + +// STEP_START query_multi +// Create three new "rg:" time series (two in the US +// and one in the UK, with different units) and add some +// data points. +const res20 = await client.ts.create('rg:2', { + LABELS: { location: 'us', unit: 'cm' } +}); +console.log(res20); // >>> OK + +const res21 = await client.ts.create('rg:3', { + LABELS: { location: 'us', unit: 'in' } +}); +console.log(res21); // >>> OK + +const res22 = await client.ts.create('rg:4', { + LABELS: { location: 'uk', unit: 'mm' } +}); +console.log(res22); // >>> OK + +const res23 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 0, value: 1.8 }, + { key: 'rg:3', timestamp: 0, value: 0.9 }, + { key: 'rg:4', timestamp: 0, value: 25 } +]); +console.log(res23); // >>> [0, 0, 0] + +const res24 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 1, value: 2.1 }, + { key: 'rg:3', timestamp: 1, value: 0.77 }, + { key: 'rg:4', timestamp: 1, value: 18 } +]); +console.log(res24); // >>> [1, 1, 1] + +const res25 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 2, value: 2.3 }, + { key: 'rg:3', timestamp: 2, value: 1.1 }, + { key: 'rg:4', timestamp: 2, value: 21 } +]); +console.log(res25); // >>> [2, 2, 2] + +const res26 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 3, value: 1.9 }, + { key: 'rg:3', timestamp: 3, value: 0.81 }, + { key: 'rg:4', timestamp: 3, value: 19 } +]); +console.log(res26); // >>> [3, 3, 3] + +const res27 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 4, value: 1.78 }, + { key: 'rg:3', timestamp: 4, value: 0.74 }, + { key: 'rg:4', timestamp: 4, value: 23 } +]); +console.log(res27); // >>> [4, 4, 4] + +// Retrieve the last data point from each US time series. +const res28 = await client.ts.mGet(['location=us']); +console.log(res28); +// >>> { "rg:2": { sample: { timestamp: 4, value: 1.78 } }, "rg:3": { sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve the same data points, but include the `unit` +// label in the results. +const res29 = await client.ts.mGetSelectedLabels(['location=us'], ['unit']); +console.log(res29); +// >>> { "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve data points up to time 2 (inclusive) from all +// time series that use millimeters as the unit. Include all +// labels in the results. +const res30 = await client.ts.mRangeWithLabels('-', 2, 'unit=mm'); +console.log(res30); +// >>> { "rg:4": { labels: { location: 'uk', unit: 'mm' }, samples: [ +// { timestamp: 0, value: 25 }, +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 } +// ] } } + +// Retrieve data points from time 1 to time 3 (inclusive) from +// all time series that use centimeters or millimeters as the unit, +// but only return the `location` label. Return the results +// in descending order of timestamp. +const res31 = await client.ts.mRevRangeSelectedLabels( + 1, 3, + ['location'], + ['unit=(cm,mm)'] +); +console.log(res31); +// >>> { "rg:2": { labels: { location: 'us' }, samples: [ +// { timestamp: 3, value: 1.9 }, +// { timestamp: 2, value: 2.3 }, +// { timestamp: 1, value: 2.1 } +// ] }, "rg:4": { labels: { location: 'uk' }, samples: [ +// { timestamp: 3, value: 19 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 1, value: 18 } +// ] } } +// STEP_END +// REMOVE_START +assert.equal(res20, 'OK'); +assert.equal(res21, 'OK'); +assert.equal(res22, 'OK'); +assert.deepEqual(res23, [0, 0, 0]); +assert.deepEqual(res24, [1, 1, 1]); +assert.deepEqual(res25, [2, 2, 2]); +assert.deepEqual(res26, [3, 3, 3]); +assert.deepEqual(res27, [4, 4, 4]); + +assert.deepEqual(res28, { + "rg:2": { sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { sample: { timestamp: 4, value: 0.74 } } +}); +assert.deepEqual(res29, { + "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } +}); + +assert.deepEqual(res30, { + "rg:4": { + labels: { location: 'uk', unit: 'mm' }, + samples: [ + { timestamp: 0, value: 25 }, + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 } + ] + } +}); +assert.deepEqual(res31, { + "rg:2": { + labels: { location: 'us' }, + samples: [ + { timestamp: 3, value: 1.9 }, + { timestamp: 2, value: 2.3 }, + { timestamp: 1, value: 2.1 } + ] + }, + "rg:4": { + labels: { location: 'uk' }, + samples: [ + { timestamp: 3, value: 19 }, + { timestamp: 2, value: 21 }, + { timestamp: 1, value: 18 } + ] + } +}); +// REMOVE_END + +// STEP_START agg +const res32 = await client.ts.range('rg:2', '-', '+', { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 2 + } +}); +console.log(res32); +// >>> [{ timestamp: 0, value: 1.9500000000000002 },{ timestamp: 2, value: 2.0999999999999996 }, { timestamp: 4, value: 1.78 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res32, [ + { timestamp: 0, value: 1.9500000000000002 }, + { timestamp: 2, value: 2.0999999999999996 }, + { timestamp: 4, value: 1.78 } +]); +// REMOVE_END + +// STEP_START agg_bucket +const res33 = await client.ts.create('sensor3'); +console.log(res33); // >>> OK + +const res34 = await client.ts.mAdd([ + { key: 'sensor3', timestamp: 10, value: 1000 }, + { key: 'sensor3', timestamp: 20, value: 2000 }, + { key: 'sensor3', timestamp: 30, value: 3000 }, + { key: 'sensor3', timestamp: 40, value: 4000 }, + { key: 'sensor3', timestamp: 50, value: 5000 }, + { key: 'sensor3', timestamp: 60, value: 6000 }, + { key: 'sensor3', timestamp: 70, value: 7000 } +]); +console.log(res34); // >>> [10, 20, 30, 40, 50, 60, 70] + +const res35 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + } +}); +console.log(res35); +// >>> [{ timestamp: 0, value: 1000 }, { timestamp: 25, value: 3000 }, { timestamp: 50, value: 5000 }] +// STEP_END +// REMOVE_START +assert.equal(res33, 'OK'); +assert.deepEqual(res34, [10, 20, 30, 40, 50, 60, 70]); +assert.deepEqual(res35, [ + { timestamp: 0, value: 1000 }, + { timestamp: 25, value: 3000 }, + { timestamp: 50, value: 5000 } +]); +// REMOVE_END + +// STEP_START agg_align +const res36 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + }, + ALIGN: 'START' +}); +console.log(res36); +// >>> [{ timestamp: 10, value: 1000 }, { timestamp: 35, value: 4000 }, { timestamp: 60, value: 6000 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res36, [ + { timestamp: 10, value: 1000 }, + { timestamp: 35, value: 4000 }, + { timestamp: 60, value: 6000 } +]); +// REMOVE_END + +// STEP_START agg_multi +const res37 = await client.ts.create('wind:1', { + LABELS: { country: 'uk' } +}); +console.log(res37); // >>> OK + +const res38 = await client.ts.create('wind:2', { + LABELS: { country: 'uk' } +}); +console.log(res38); // >>> OK + +const res39 = await client.ts.create('wind:3', { + LABELS: { country: 'us' } +}); +console.log(res39); // >>> OK + +const res40 = await client.ts.create('wind:4', { + LABELS: { country: 'us' } +}); +console.log(res40); // >>> OK + +const res41 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 1, value: 12 }, + { key: 'wind:2', timestamp: 1, value: 18 }, + { key: 'wind:3', timestamp: 1, value: 5 }, + { key: 'wind:4', timestamp: 1, value: 20 } +]); +console.log(res41); // >>> [1, 1, 1, 1] + +const res42 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 2, value: 14 }, + { key: 'wind:2', timestamp: 2, value: 21 }, + { key: 'wind:3', timestamp: 2, value: 4 }, + { key: 'wind:4', timestamp: 2, value: 25 } +]); +console.log(res42); // >>> [2, 2, 2, 2] + +const res43 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 3, value: 10 }, + { key: 'wind:2', timestamp: 3, value: 24 }, + { key: 'wind:3', timestamp: 3, value: 8 }, + { key: 'wind:4', timestamp: 3, value: 18 } +]); +console.log(res43); // >>> [3, 3, 3, 3] + +// The result pairs contain the timestamp and the maximum sample value +// for the country at that timestamp. +const res44 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + {label: 'country', REDUCE: TIME_SERIES_REDUCERS.MAX} +); +console.log(res44); +// >>> { "country=uk": { samples: [ +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 3, value: 24 } +// ] }, "country=us": { samples: [ +// { timestamp: 1, value: 20 }, +// { timestamp: 2, value: 25 }, +// { timestamp: 3, value: 18 } +// ] } } + +// The result pairs contain the timestamp and the average sample value +// for the country at that timestamp. +const res45 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + { label: 'country', REDUCE: TIME_SERIES_REDUCERS.AVG} +); +console.log(res45); +// >>> { +// "country=uk": { +// samples: [{ timestamp: 1, value: 15 }, { timestamp: 2, value: 17.5 }, { timestamp: 3, value: 17 }] +// }, +// "country=us": { +// samples: [{ timestamp: 1, value: 12.5 }, { timestamp: 2, value: 14.5 }, { timestamp: 3, value: 13 }] +// } +// } +// STEP_END +// REMOVE_START +assert.equal(res37, 'OK'); +assert.equal(res38, 'OK'); +assert.equal(res39, 'OK'); +assert.equal(res40, 'OK'); +assert.deepEqual(res41, [1, 1, 1, 1]); +assert.deepEqual(res42, [2, 2, 2, 2]); +assert.deepEqual(res43, [3, 3, 3, 3]); + +assert.deepEqual(res44, { + "country=uk": { + samples: [ + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 }, + { timestamp: 3, value: 24 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 20 }, + { timestamp: 2, value: 25 }, + { timestamp: 3, value: 18 } + ] + } +}); +assert.deepEqual(res45, { + "country=uk": { + samples: [ + { timestamp: 1, value: 15 }, + { timestamp: 2, value: 17.5 }, + { timestamp: 3, value: 17 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 12.5 }, + { timestamp: 2, value: 14.5 }, + { timestamp: 3, value: 13 } + ] + } +}); +// REMOVE_END + +// STEP_START create_compaction +const res46 = await client.ts.create('hyg:1'); +console.log(res46); // >>> OK + +const res47 = await client.ts.create('hyg:compacted'); +console.log(res47); // >>> OK + +const res48 = await client.ts.createRule('hyg:1', 'hyg:compacted', TIME_SERIES_AGGREGATION_TYPE.MIN, 3); +console.log(res48); // >>> OK + +const res49 = await client.ts.info('hyg:1'); +console.log(res49.rules); +// >>> [{ aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3}] + +const res50 = await client.ts.info('hyg:compacted'); +console.log(res50.sourceKey); // >>> 'hyg:1' +// STEP_END +// REMOVE_START +assert.equal(res46, 'OK'); +assert.equal(res47, 'OK'); +assert.equal(res48, 'OK'); +assert.deepEqual(res49.rules, [ + { aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3} +]); +assert.equal(res50.sourceKey, 'hyg:1'); +// REMOVE_END + +// STEP_START comp_add +const res51 = await client.ts.mAdd([ + { key: 'hyg:1', timestamp: 0, value: 75 }, + { key: 'hyg:1', timestamp: 1, value: 77 }, + { key: 'hyg:1', timestamp: 2, value: 78 } +]); +console.log(res51); // >>> [0, 1, 2] + +const res52 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res52); // >>> [] + +const res53 = await client.ts.add('hyg:1', 3, 79); +console.log(res53); // >>> 3 + +const res54 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res54); // >>> [{ timestamp: 0, value: 75 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res51, [0, 1, 2]); +assert.deepEqual(res52, []); +assert.equal(res53, 3); +assert.deepEqual(res54, [{ timestamp: 0, value: 75 }]); +// REMOVE_END + +// STEP_START del +const res55 = await client.ts.info('thermometer:1'); +console.log(res55.totalSamples); // >>> 2 +console.log(res55.firstTimestamp); // >>> 1 +console.log(res55.lastTimestamp); // >>> 2 + +const res56 = await client.ts.add('thermometer:1', 3, 9.7); +console.log(res56); // >>> 3 + +const res57 = await client.ts.info('thermometer:1'); +console.log(res57.totalSamples); // >>> 3 +console.log(res57.firstTimestamp); // >>> 1 +console.log(res57.lastTimestamp); // >>> 3 + +const res58 = await client.ts.del('thermometer:1', 1, 2); +console.log(res58); // >>> 2 + +const res59 = await client.ts.info('thermometer:1'); +console.log(res59.totalSamples); // >>> 1 +console.log(res59.firstTimestamp); // >>> 3 +console.log(res59.lastTimestamp); // >>> 3 + +const res60 = await client.ts.del('thermometer:1', 3, 3); +console.log(res60); // >>> 1 + +const res61 = await client.ts.info('thermometer:1'); +console.log(res61.totalSamples); // >>> 0 +// STEP_END +// REMOVE_START +assert.equal(res55.totalSamples, 2); +assert.equal(res55.firstTimestamp, 1); +assert.equal(res55.lastTimestamp, 2); +assert.equal(res56, 3); +assert.equal(res57.totalSamples, 3); +assert.equal(res57.firstTimestamp, 1); +assert.equal(res57.lastTimestamp, 3); +assert.equal(res58, 2); +assert.equal(res59.totalSamples, 1); +assert.equal(res59.firstTimestamp, 3); +assert.equal(res59.lastTimestamp, 3); +assert.equal(res60, 1); +assert.equal(res61.totalSamples, 0); +// REMOVE_END + +// HIDE_START +await client.quit(); +// HIDE_END \ No newline at end of file From 28d719d699b41023fa63ba616cc6931d927d5132 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:01:05 +0100 Subject: [PATCH 04/13] docs: DOC-5074 added vector set doc examples (#3031) --- doctests/dt-vec-set.js | 281 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 doctests/dt-vec-set.js diff --git a/doctests/dt-vec-set.js b/doctests/dt-vec-set.js new file mode 100644 index 00000000000..0e8cb918d7e --- /dev/null +++ b/doctests/dt-vec-set.js @@ -0,0 +1,281 @@ +// EXAMPLE: vecset_tutorial +// REMOVE_START +/** + * Code samples for Vector set doc pages: + * https://redis.io/docs/latest/develop/data-types/vector-sets/ + */ + +import assert from 'assert'; +// REMOVE_END +// HIDE_START +import { createClient } from 'redis'; + +const client = createClient({ + RESP: 3 // Required for vector set commands +}); + +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del([ + "points", "quantSetQ8", "quantSetNoQ", + "quantSetBin", "setNotReduced", "setReduced" +]); +// REMOVE_END + +// STEP_START vadd +const res1 = await client.vAdd("points", [1.0, 1.0], "pt:A"); +console.log(res1); // >>> true + +const res2 = await client.vAdd("points", [-1.0, -1.0], "pt:B"); +console.log(res2); // >>> true + +const res3 = await client.vAdd("points", [-1.0, 1.0], "pt:C"); +console.log(res3); // >>> true + +const res4 = await client.vAdd("points", [1.0, -1.0], "pt:D"); +console.log(res4); // >>> true + +const res5 = await client.vAdd("points", [1.0, 0], "pt:E"); +console.log(res5); // >>> true + +const res6 = await client.type("points"); +console.log(res6); // >>> vectorset +// STEP_END +// REMOVE_START +assert.equal(res1, true); +assert.equal(res2, true); +assert.equal(res3, true); +assert.equal(res4, true); +assert.equal(res5, true); +assert.equal(res6, "vectorset"); +// REMOVE_END + +// STEP_START vcardvdim +const res7 = await client.vCard("points"); +console.log(res7); // >>> 5 + +const res8 = await client.vDim("points"); +console.log(res8); // >>> 2 +// STEP_END +// REMOVE_START +assert.equal(res7, 5); +assert.equal(res8, 2); +// REMOVE_END + +// STEP_START vemb +const res9 = await client.vEmb("points", "pt:A"); +console.log(res9); // >>> [0.9999999403953552, 0.9999999403953552] + +const res10 = await client.vEmb("points", "pt:B"); +console.log(res10); // >>> [-0.9999999403953552, -0.9999999403953552] + +const res11 = await client.vEmb("points", "pt:C"); +console.log(res11); // >>> [-0.9999999403953552, 0.9999999403953552] + +const res12 = await client.vEmb("points", "pt:D"); +console.log(res12); // >>> [0.9999999403953552, -0.9999999403953552] + +const res13 = await client.vEmb("points", "pt:E"); +console.log(res13); // >>> [1, 0] +// STEP_END +// REMOVE_START +assert(Math.abs(1 - res9[0]) < 0.001); +assert(Math.abs(1 - res9[1]) < 0.001); +assert(Math.abs(1 + res10[0]) < 0.001); +assert(Math.abs(1 + res10[1]) < 0.001); +assert(Math.abs(1 + res11[0]) < 0.001); +assert(Math.abs(1 - res11[1]) < 0.001); +assert(Math.abs(1 - res12[0]) < 0.001); +assert(Math.abs(1 + res12[1]) < 0.001); +assert.deepEqual(res13, [1, 0]); +// REMOVE_END + +// STEP_START attr +const res14 = await client.vSetAttr("points", "pt:A", { + name: "Point A", + description: "First point added" +}); +console.log(res14); // >>> true + +const res15 = await client.vGetAttr("points", "pt:A"); +console.log(res15); +// >>> {name: 'Point A', description: 'First point added'} + +const res16 = await client.vSetAttr("points", "pt:A", ""); +console.log(res16); // >>> true + +const res17 = await client.vGetAttr("points", "pt:A"); +console.log(res17); // >>> null +// STEP_END +// REMOVE_START +assert.equal(res14, true); +assert.deepEqual(res15, {name: "Point A", description: "First point added"}); +assert.equal(res16, true); +assert.equal(res17, null); +// REMOVE_END + +// STEP_START vrem +const res18 = await client.vAdd("points", [0, 0], "pt:F"); +console.log(res18); // >>> true + +const res19 = await client.vCard("points"); +console.log(res19); // >>> 6 + +const res20 = await client.vRem("points", "pt:F"); +console.log(res20); // >>> true + +const res21 = await client.vCard("points"); +console.log(res21); // >>> 5 +// STEP_END +// REMOVE_START +assert.equal(res18, true); +assert.equal(res19, 6); +assert.equal(res20, true); +assert.equal(res21, 5); +// REMOVE_END + +// STEP_START vsim_basic +const res22 = await client.vSim("points", [0.9, 0.1]); +console.log(res22); +// >>> ['pt:E', 'pt:A', 'pt:D', 'pt:C', 'pt:B'] +// STEP_END +// REMOVE_START +assert.deepEqual(res22, ["pt:E", "pt:A", "pt:D", "pt:C", "pt:B"]); +// REMOVE_END + +// STEP_START vsim_options +const res23 = await client.vSimWithScores("points", "pt:A", { COUNT: 4 }); +console.log(res23); +// >>> {pt:A: 1.0, pt:E: 0.8535534143447876, pt:D: 0.5, pt:C: 0.5} +// STEP_END +// REMOVE_START +assert.equal(res23["pt:A"], 1.0); +assert.equal(res23["pt:C"], 0.5); +assert.equal(res23["pt:D"], 0.5); +assert(Math.abs(res23["pt:E"] - 0.85) < 0.005); +// REMOVE_END + +// STEP_START vsim_filter +const res24 = await client.vSetAttr("points", "pt:A", { + size: "large", + price: 18.99 +}); +console.log(res24); // >>> true + +const res25 = await client.vSetAttr("points", "pt:B", { + size: "large", + price: 35.99 +}); +console.log(res25); // >>> true + +const res26 = await client.vSetAttr("points", "pt:C", { + size: "large", + price: 25.99 +}); +console.log(res26); // >>> true + +const res27 = await client.vSetAttr("points", "pt:D", { + size: "small", + price: 21.00 +}); +console.log(res27); // >>> true + +const res28 = await client.vSetAttr("points", "pt:E", { + size: "small", + price: 17.75 +}); +console.log(res28); // >>> true + +// Return elements in order of distance from point A whose +// `size` attribute is `large`. +const res29 = await client.vSim("points", "pt:A", { + FILTER: '.size == "large"' +}); +console.log(res29); // >>> ['pt:A', 'pt:C', 'pt:B'] + +// Return elements in order of distance from point A whose size is +// `large` and whose price is greater than 20.00. +const res30 = await client.vSim("points", "pt:A", { + FILTER: '.size == "large" && .price > 20.00' +}); +console.log(res30); // >>> ['pt:C', 'pt:B'] +// STEP_END +// REMOVE_START +assert.equal(res24, true); +assert.equal(res25, true); +assert.equal(res26, true); +assert.equal(res27, true); +assert.equal(res28, true); +assert.deepEqual(res29, ['pt:A', 'pt:C', 'pt:B']); +assert.deepEqual(res30, ['pt:C', 'pt:B']); +// REMOVE_END + +// STEP_START add_quant +const res31 = await client.vAdd("quantSetQ8", [1.262185, 1.958231], "quantElement", { + QUANT: 'Q8' +}); +console.log(res31); // >>> true + +const res32 = await client.vEmb("quantSetQ8", "quantElement"); +console.log(`Q8: ${res32}`); +// >>> Q8: [1.2643694877624512, 1.958230972290039] + +const res33 = await client.vAdd("quantSetNoQ", [1.262185, 1.958231], "quantElement", { + QUANT: 'NOQUANT' +}); +console.log(res33); // >>> true + +const res34 = await client.vEmb("quantSetNoQ", "quantElement"); +console.log(`NOQUANT: ${res34}`); +// >>> NOQUANT: [1.262184977531433, 1.958230972290039] + +const res35 = await client.vAdd("quantSetBin", [1.262185, 1.958231], "quantElement", { + QUANT: 'BIN' +}); +console.log(res35); // >>> true + +const res36 = await client.vEmb("quantSetBin", "quantElement"); +console.log(`BIN: ${res36}`); +// >>> BIN: [1, 1] +// STEP_END +// REMOVE_START +assert.equal(res31, true); +assert(Math.abs(res32[0] - 1.2643694877624512) < 0.001); +assert(Math.abs(res32[1] - 1.958230972290039) < 0.001); +assert.equal(res33, true); +assert(Math.abs(res34[0] - 1.262184977531433) < 0.001); +assert(Math.abs(res34[1] - 1.958230972290039) < 0.001); +assert.equal(res35, true); +assert.deepEqual(res36, [1, 1]); +// REMOVE_END + +// STEP_START add_reduce +// Create a list of 300 arbitrary values. +const values = Array.from({length: 300}, (_, x) => x / 299); + +const res37 = await client.vAdd("setNotReduced", values, "element"); +console.log(res37); // >>> true + +const res38 = await client.vDim("setNotReduced"); +console.log(res38); // >>> 300 + +const res39 = await client.vAdd("setReduced", values, "element", { + REDUCE: 100 +}); +console.log(res39); // >>> true + +const res40 = await client.vDim("setReduced"); +console.log(res40); // >>> 100 +// STEP_END +// REMOVE_START +assert.equal(res37, true); +assert.equal(res38, 300); +assert.equal(res39, true); +assert.equal(res40, 100); +// REMOVE_END + +// HIDE_START +await client.quit(); +// HIDE_END From d8e14fa4febe69cfee942a477f99dd2492e8ff18 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 28 Jul 2025 14:54:50 +0300 Subject: [PATCH 05/13] fix(scan): remove console.logs (#3038) fixes #3037 --- packages/client/lib/commands/SCAN.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 173e8959afa..d3153b786f1 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -87,7 +87,6 @@ export default { if (options?.TYPE) { parser.push('TYPE', options.TYPE); } - console.log('eeeeeeeeee', parser.redisArgs) }, /** * Transforms the SCAN reply into a structured object @@ -96,7 +95,6 @@ export default { * @returns Object with cursor and keys properties */ transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { - console.log(cursor, keys) return { cursor, keys From 5f09e4a8a58cfaa093a9860724f812c1c19e0d33 Mon Sep 17 00:00:00 2001 From: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:11:51 +0300 Subject: [PATCH 06/13] feat: add EPSILON parameter support to VSIM command (#3041) --- packages/client/lib/commands/VSIM.spec.ts | 30 ++++++++++++++++++++--- packages/client/lib/commands/VSIM.ts | 5 ++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/commands/VSIM.spec.ts b/packages/client/lib/commands/VSIM.spec.ts index b7e10eb6c49..dbfad76fd59 100644 --- a/packages/client/lib/commands/VSIM.spec.ts +++ b/packages/client/lib/commands/VSIM.spec.ts @@ -31,14 +31,15 @@ describe('VSIM', () => { FILTER: '.price > 20', 'FILTER-EF': 50, TRUTH: true, - NOTHREAD: true + NOTHREAD: true, + EPSILON: 0.1 }); assert.deepEqual( parser.redisArgs, [ - 'VSIM', 'key', 'ELE', 'element', - 'COUNT', '5', 'EF', '100', 'FILTER', '.price > 20', - 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD' + 'VSIM', 'key', 'ELE', 'element', 'COUNT', '5', + 'EPSILON', '0.1', 'EF', '100', 'FILTER', '.price > 20', + 'FILTER-EF', '50', 'TRUTH', 'NOTHREAD', ] ); }); @@ -56,6 +57,27 @@ describe('VSIM', () => { cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } }); + + testUtils.testAll('vSim with options', async client => { + await client.vAdd('key', [1.0, 2.0, 3.0], 'element1'); + await client.vAdd('key', [1.1, 2.1, 3.1], 'element2'); + + const result = await client.vSim('key', 'element1', { + EPSILON: 0.1, + COUNT: 1, + EF: 100, + FILTER: '.year == 8', + 'FILTER-EF': 50, + TRUTH: true, + NOTHREAD: true + }); + + assert.ok(Array.isArray(result)); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] } + }); + testUtils.testWithClient('vSim with RESP3', async client => { await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1'); await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2'); diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts index dc41a54caf1..7c94cd7c79d 100644 --- a/packages/client/lib/commands/VSIM.ts +++ b/packages/client/lib/commands/VSIM.ts @@ -4,6 +4,7 @@ import { transformDoubleArgument } from './generic-transformers'; export interface VSimOptions { COUNT?: number; + EPSILON?: number; EF?: number; FILTER?: string; 'FILTER-EF'?: number; @@ -44,6 +45,10 @@ export default { parser.push('COUNT', options.COUNT.toString()); } + if (options?.EPSILON !== undefined) { + parser.push('EPSILON', options.EPSILON.toString()); + } + if (options?.EF !== undefined) { parser.push('EF', options.EF.toString()); } From facc91847e5605fbe8ecaa8dd44a8b913c495e57 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:03 +0000 Subject: [PATCH 07/13] Release client@5.7.0 --- package-lock.json | 14 +++++++++++++- packages/client/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71e0ce79df8..640861530b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7351,7 +7351,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", + "integrity": "sha512-bWHmSFIJ5w1Y4aHsYs46XMDHKQsBHFRhNcllYaBxz2Zl+lu+gbm5yI9BqxvKh48bLTs/Wx1Kns0gN2WIasE8MA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.1", diff --git a/packages/client/package.json b/packages/client/package.json index 9fe67df521e..091ddaf5c91 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 69fa90784344b3e5e7b26b2925effbe288f974a4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:11 +0000 Subject: [PATCH 08/13] Release bloom@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 640861530b2..79a9493d3c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,7 +7346,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/client": { @@ -7449,6 +7449,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.1.tgz", + "integrity": "sha512-5/22U76IMEfn6TeZ+uvjXspHw+ykBF0kpBa8xouzeHaQMXs/auqBUOEYzU2VKYDvnd2RSpPTyIg82oB7PpUgLg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index ef900b00a6d..edc106214e0 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From b5cf002751853bb8fe17207efbd15a66b4958e93 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:18 +0000 Subject: [PATCH 09/13] Release json@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/json/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79a9493d3c8..2fda06e6a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,7 +7432,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/redis": { @@ -7473,6 +7473,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.1.tgz", + "integrity": "sha512-cTggVzPIVuiFeXcEcnTRiUzV7rmUvM9KUYxWiHyjsAVACTEUe4ifKkvzrij0H/z3ammU5tfGACffDB3olBwtVA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.6.1", diff --git a/packages/json/package.json b/packages/json/package.json index e62377feb14..fac6209c360 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From f37f98aced7dd76b55c2b39b053b0e05b1f98806 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:26 +0000 Subject: [PATCH 10/13] Release search@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fda06e6a8c..52d8b4f2a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7485,9 +7485,21 @@ "@redis/client": "^5.6.1" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.1.tgz", + "integrity": "sha512-+eOjx8O2YoKygjqkLpTHqcAq0zKLjior+ee2tRBx/3RSf1+OHxiC9Y6NstshQpvB1XHqTw9n7+f0+MsRJZrp0g==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7508,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index c8fb49d5332..5d7fffcfd26 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From fa3ca983a3e1fda3a81f254ba9dd82cd6af12df4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:34 +0000 Subject: [PATCH 11/13] Release time-series@5.7.0 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52d8b4f2a6e..c9e584024b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7497,6 +7497,18 @@ "@redis/client": "^5.6.1" } }, + "packages/redis/node_modules/@redis/time-series": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.1.tgz", + "integrity": "sha512-sd3q4jMJdoSO2akw1L9NrdFI1JJ6zeMgMUoTh4a34p9sY3AnOI4aDLCecy8L2IcPAP1oNR3TbLFJiCJDQ35QTA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.1" + } + }, "packages/search": { "name": "@redis/search", "version": "5.7.0", @@ -7577,7 +7589,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7586,7 +7598,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 1c42757523f..44fca5643f3 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@redis/test-utils": "*" From c75809ec650883a33ddcdca365c91aa5fbbf4961 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:41 +0000 Subject: [PATCH 12/13] Release entraid@5.7.0 --- package-lock.json | 4 ++-- packages/entraid/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9e584024b2..eacb323d1a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" } }, "packages/entraid/node_modules/@types/node": { diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 4e702fabb65..cb57d9074c3 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.6.1" + "@redis/client": "^5.7.0" }, "devDependencies": { "@types/express": "^4.17.21", From e2d4b43e39d145e605ca582dbb520b94ab505117 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 31 Jul 2025 13:39:49 +0000 Subject: [PATCH 13/13] Release redis@5.7.0 --- package-lock.json | 72 ++++--------------------------------- packages/redis/package.json | 12 +++---- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index eacb323d1a5..5ecc96aff06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7436,79 +7436,19 @@ } }, "packages/redis": { - "version": "5.6.1", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.6.1", - "@redis/client": "5.6.1", - "@redis/json": "5.6.1", - "@redis/search": "5.6.1", - "@redis/time-series": "5.6.1" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.1.tgz", - "integrity": "sha512-5/22U76IMEfn6TeZ+uvjXspHw+ykBF0kpBa8xouzeHaQMXs/auqBUOEYzU2VKYDvnd2RSpPTyIg82oB7PpUgLg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz", - "integrity": "sha512-bWHmSFIJ5w1Y4aHsYs46XMDHKQsBHFRhNcllYaBxz2Zl+lu+gbm5yI9BqxvKh48bLTs/Wx1Kns0gN2WIasE8MA==", + "version": "5.7.0", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "@redis/bloom": "5.7.0", + "@redis/client": "5.7.0", + "@redis/json": "5.7.0", + "@redis/search": "5.7.0", + "@redis/time-series": "5.7.0" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/json": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.1.tgz", - "integrity": "sha512-cTggVzPIVuiFeXcEcnTRiUzV7rmUvM9KUYxWiHyjsAVACTEUe4ifKkvzrij0H/z3ammU5tfGACffDB3olBwtVA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.1.tgz", - "integrity": "sha512-+eOjx8O2YoKygjqkLpTHqcAq0zKLjior+ee2tRBx/3RSf1+OHxiC9Y6NstshQpvB1XHqTw9n7+f0+MsRJZrp0g==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, - "packages/redis/node_modules/@redis/time-series": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.1.tgz", - "integrity": "sha512-sd3q4jMJdoSO2akw1L9NrdFI1JJ6zeMgMUoTh4a34p9sY3AnOI4aDLCecy8L2IcPAP1oNR3TbLFJiCJDQ35QTA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.6.1" - } - }, "packages/search": { "name": "@redis/search", "version": "5.7.0", diff --git a/packages/redis/package.json b/packages/redis/package.json index bdb86663480..a14f6ba10bc 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.6.1", + "version": "5.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.6.1", - "@redis/client": "5.6.1", - "@redis/json": "5.6.1", - "@redis/search": "5.6.1", - "@redis/time-series": "5.6.1" + "@redis/bloom": "5.7.0", + "@redis/client": "5.7.0", + "@redis/json": "5.7.0", + "@redis/search": "5.7.0", + "@redis/time-series": "5.7.0" }, "engines": { "node": ">= 18"