From b8acbce213c0953f7b5874a8d0d1738805864e04 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 16:22:20 +0300 Subject: [PATCH 01/19] udpate package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index a9f846e7a8..ab4cbe138b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8619,7 +8619,7 @@ } }, "packages/redis": { - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@redis/bloom": "5.1.0", From 9ea260f0f9a3aba90c0c62ade443c23d5c44ab31 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 21 May 2025 11:38:29 +0300 Subject: [PATCH 02/19] fix(handshake): ignore errors on client.setinfo (#2969) As per the documentation (https://redis.io/docs/latest/commands/client-setinfo): Client libraries are expected to pipeline this command after authentication on all connections and ignore failures since they could be connected to an older version that doesn't support them. Turns out different versions of redis server return different errors, so its better to catch all. fixes #2968 --- packages/client/lib/client/index.ts | 26 +++++++++----------------- packages/client/lib/errors.ts | 6 +----- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 8d98aa8ed2..a446ad8e75 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -4,7 +4,7 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; -import { ClientClosedError, ClientOfflineError, DisconnectsClientError, SimpleError, WatchError } from '../errors'; +import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors'; import { URL } from 'node:url'; import { TcpSocketConnectOpts } from 'node:net'; import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; @@ -626,14 +626,10 @@ export default class RedisClient< if (!this.#options?.disableClientInfo) { commands.push({ cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); @@ -646,14 +642,10 @@ export default class RedisClient< ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis' ], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); } diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index 4f05f62ac4..db37ec1a9b 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -70,11 +70,7 @@ export class ErrorReply extends Error { } } -export class SimpleError extends ErrorReply { - isUnknownSubcommand(): boolean { - return this.message.toLowerCase().indexOf('err unknown subcommand') !== -1; - } -} +export class SimpleError extends ErrorReply {} export class BlobError extends ErrorReply {} From 27537b0ab79b3a0dae4db1f03b5bb6eefacb32fb Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 22 May 2025 10:35:16 +0300 Subject: [PATCH 03/19] fix(cluster): replace native private with _ (#2971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Private class fields (e.g., #execute) cause runtime errors when accessed from contexts where `this` is not the exact instance, triggering “Receiver must be an instance of class RedisCluster”. fixes #2967 --- packages/client/lib/cluster/index.ts | 96 ++++++++++++++-------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index f738562926..c2c251810e 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -188,7 +188,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._commandOptions, @@ -204,7 +204,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._self._commandOptions, @@ -222,7 +222,7 @@ export default class RedisCluster< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, fn.IS_READ_ONLY, this._self._commandOptions, @@ -240,7 +240,7 @@ export default class RedisCluster< parser.push(...prefix); script.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, script.IS_READ_ONLY, this._commandOptions, @@ -293,9 +293,9 @@ export default class RedisCluster< return RedisCluster.factory(options)(options); } - readonly #options: RedisClusterOptions; + readonly _options: RedisClusterOptions; - readonly #slots: RedisClusterSlots; + readonly _slots: RedisClusterSlots; private _self = this; private _commandOptions?: ClusterCommandOptions; @@ -305,11 +305,11 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get slots() { - return this._self.#slots.slots; + return this._self._slots.slots; } get clientSideCache() { - return this._self.#slots.clientSideCache; + return this._self._slots.clientSideCache; } /** @@ -317,7 +317,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node. */ get masters() { - return this._self.#slots.masters; + return this._self._slots.masters; } /** @@ -325,7 +325,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node. */ get replicas() { - return this._self.#slots.replicas; + return this._self._slots.replicas; } /** @@ -333,25 +333,25 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get nodeByAddress() { - return this._self.#slots.nodeByAddress; + return this._self._slots.nodeByAddress; } /** * The current pub/sub node. */ get pubSubNode() { - return this._self.#slots.pubSubNode; + return this._self._slots.pubSubNode; } get isOpen() { - return this._self.#slots.isOpen; + return this._self._slots.isOpen; } constructor(options: RedisClusterOptions) { super(); - this.#options = options; - this.#slots = new RedisClusterSlots(options, this.emit.bind(this)); + this._options = options; + this._slots = new RedisClusterSlots(options, this.emit.bind(this)); if (options?.commandOptions) { this._commandOptions = options.commandOptions; @@ -366,14 +366,14 @@ export default class RedisCluster< _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING >(overrides?: Partial>) { return new (Object.getPrototypeOf(this).constructor)({ - ...this._self.#options, + ...this._self._options, commandOptions: this._commandOptions, ...overrides }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>; } async connect() { - await this._self.#slots.connect(); + await this._self._slots.connect(); return this as unknown as RedisClusterType; } @@ -429,7 +429,7 @@ export default class RedisCluster< // return this._commandOptionsProxy('policies', policies); // } - #handleAsk( + _handleAsk( fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ) { return async (client: RedisClientType, options?: ClusterCommandOptions) => { @@ -450,14 +450,14 @@ export default class RedisCluster< }; } - async #execute( + async _execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, options: ClusterCommandOptions | undefined, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { - const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; - let client = await this.#slots.getClient(firstKey, isReadonly); + const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; + let client = await this._slots.getClient(firstKey, isReadonly); let i = 0; let myFn = fn; @@ -475,10 +475,10 @@ export default class RedisCluster< if (err.message.startsWith('ASK')) { const address = err.message.substring(err.message.lastIndexOf(' ') + 1); - let redirectTo = await this.#slots.getMasterByAddress(address); + let redirectTo = await this._slots.getMasterByAddress(address); if (!redirectTo) { - await this.#slots.rediscover(client); - redirectTo = await this.#slots.getMasterByAddress(address); + await this._slots.rediscover(client); + redirectTo = await this._slots.getMasterByAddress(address); } if (!redirectTo) { @@ -486,13 +486,13 @@ export default class RedisCluster< } client = redirectTo; - myFn = this.#handleAsk(fn); + myFn = this._handleAsk(fn); continue; } if (err.message.startsWith('MOVED')) { - await this.#slots.rediscover(client); - client = await this.#slots.getClient(firstKey, isReadonly); + await this._slots.rediscover(client); + client = await this._slots.getClient(firstKey, isReadonly); continue; } @@ -508,7 +508,7 @@ export default class RedisCluster< options?: ClusterCommandOptions, // defaultPolicies?: CommandPolicies ): Promise { - return this._self.#execute( + return this._self._execute( firstKey, isReadonly, options, @@ -520,11 +520,11 @@ export default class RedisCluster< type Multi = new (...args: ConstructorParameters) => RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; return new ((this as any).Multi as Multi)( async (firstKey, isReadonly, commands) => { - const client = await this._self.#slots.getClient(firstKey, isReadonly); + const client = await this._self._slots.getClient(firstKey, isReadonly); return client._executeMulti(commands); }, async (firstKey, isReadonly, commands) => { - const client = await this._self.#slots.getClient(firstKey, isReadonly); + const client = await this._self._slots.getClient(firstKey, isReadonly); return client._executePipeline(commands); }, routing, @@ -539,7 +539,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .SUBSCRIBE(channels, listener, bufferMode); } @@ -550,7 +550,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.UNSUBSCRIBE(channels, listener, bufferMode) ); } @@ -562,7 +562,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .PSUBSCRIBE(patterns, listener, bufferMode); } @@ -573,7 +573,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.PUNSUBSCRIBE(patterns, listener, bufferMode) ); } @@ -585,9 +585,9 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - const maxCommandRedirections = this._self.#options.maxCommandRedirections ?? 16, + const maxCommandRedirections = this._self._options.maxCommandRedirections ?? 16, firstChannel = Array.isArray(channels) ? channels[0] : channels; - let client = await this._self.#slots.getShardedPubSubClient(firstChannel); + let client = await this._self._slots.getShardedPubSubClient(firstChannel); for (let i = 0; ; i++) { try { return await client.SSUBSCRIBE(channels, listener, bufferMode); @@ -597,8 +597,8 @@ export default class RedisCluster< } if (err.message.startsWith('MOVED')) { - await this._self.#slots.rediscover(client); - client = await this._self.#slots.getShardedPubSubClient(firstChannel); + await this._self._slots.rediscover(client); + client = await this._self._slots.getShardedPubSubClient(firstChannel); continue; } @@ -614,7 +614,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeShardedUnsubscribeCommand( + return this._self._slots.executeShardedUnsubscribeCommand( Array.isArray(channels) ? channels[0] : channels, client => client.SUNSUBSCRIBE(channels, listener, bufferMode) ); @@ -626,28 +626,28 @@ export default class RedisCluster< * @deprecated Use `close` instead. */ quit() { - return this._self.#slots.quit(); + return this._self._slots.quit(); } /** * @deprecated Use `destroy` instead. */ disconnect() { - return this._self.#slots.disconnect(); + return this._self._slots.disconnect(); } close() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.close(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.close(); } destroy() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.destroy(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.destroy(); } nodeClient(node: ShardNode) { - return this._self.#slots.nodeClient(node); + return this._self._slots.nodeClient(node); } /** @@ -655,7 +655,7 @@ export default class RedisCluster< * Userful for running "forward" commands (like PUBLISH) on a random node. */ getRandomNode() { - return this._self.#slots.getRandomNode(); + return this._self._slots.getRandomNode(); } /** @@ -663,7 +663,7 @@ export default class RedisCluster< * Useful for running readonly commands on a slot. */ getSlotRandomNode(slot: number) { - return this._self.#slots.getSlotRandomNode(slot); + return this._self._slots.getSlotRandomNode(slot); } /** From 065eb5e9145db9152923a44b63a356c55f76cf3b Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 22 May 2025 10:36:52 +0300 Subject: [PATCH 04/19] fix(json): remove debug console.logs (#2970) --- packages/json/lib/commands/helpers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts index 26ff12f683..99579ce81c 100644 --- a/packages/json/lib/commands/helpers.ts +++ b/packages/json/lib/commands/helpers.ts @@ -2,7 +2,6 @@ import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformer import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - console.log('transformRedisJsonNullReply', json) return isNullReply(json) ? json : transformRedisJsonReply(json); } @@ -17,6 +16,5 @@ export function transformRedisJsonArgument(json: RedisJSON): string { export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { const res = JSON.parse((json as unknown as UnwrapReply).toString()); - console.log('transformRedisJsonReply', json, res) return res; } From f346bad64e87730bfbe9a38fb639dbd759353332 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Tue, 27 May 2025 14:21:22 +0300 Subject: [PATCH 05/19] Adapt legacy sentinel tests to use the new test utils (#2976) * modified legacy sentinel tests * Adapt legacy sentinel tests to use the new test utils * modify tmpdir creation * reduced sentinel config timeouts, removed unneeded comment --------- Co-authored-by: H. Temelski --- packages/client/lib/client/index.spec.ts | 4 +- packages/client/lib/sentinel/index.spec.ts | 67 ++-- packages/client/lib/sentinel/test-util.ts | 347 ++++++++------------- packages/client/lib/test-utils.ts | 2 +- packages/test-utils/lib/dockers.ts | 81 +++-- packages/test-utils/lib/index.ts | 71 ++++- 6 files changed, 264 insertions(+), 308 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index cc052dd5b5..4f752210db 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -89,8 +89,8 @@ describe('Client', () => { && expected?.credentialsProvider?.type === 'async-credentials-provider') { // Compare the actual output of the credentials functions - const resultCreds = await result.credentialsProvider.credentials(); - const expectedCreds = await expected.credentialsProvider.credentials(); + const resultCreds = await result.credentialsProvider?.credentials(); + const expectedCreds = await expected.credentialsProvider?.credentials(); assert.deepEqual(resultCreds, expectedCreds); } else { assert.fail('Credentials provider type mismatch'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a2e52b774b..a9bdc8cc95 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -197,7 +197,6 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { await sentinel.functionLoad( @@ -377,12 +376,9 @@ describe(`test with masterPoolSize 2`, () => { }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }); - -// TODO: Figure out how to modify the test utils -// so it would have fine grained controll over -// sentinel -// it should somehow replicate the `SentinelFramework` object functionallities async function steadyState(frame: SentinelFramework) { + // wait a bit to ensure that sentinels are seeing eachother + await setTimeout(2000) let checkedMaster = false; let checkedReplicas = false; while (!checkedMaster || !checkedReplicas) { @@ -430,7 +426,7 @@ async function steadyState(frame: SentinelFramework) { } } -describe.skip('legacy tests', () => { +describe('legacy tests', () => { const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined }; const frame = new SentinelFramework(config); let tracer = new Array(); @@ -439,42 +435,30 @@ describe.skip('legacy tests', () => { let longestTestDelta = 0; let last: number; - before(async function () { - this.timeout(15000); - - last = Date.now(); - - function deltaMeasurer() { - const delta = Date.now() - last; - if (delta > longestDelta) { - longestDelta = delta; - } - if (delta > longestTestDelta) { - longestTestDelta = delta; - } - if (!stopMeasuringBlocking) { - last = Date.now(); - setImmediate(deltaMeasurer); - } - } - setImmediate(deltaMeasurer); - await frame.spawnRedisSentinel(); - }); - - after(async function () { - this.timeout(15000); - - stopMeasuringBlocking = true; - - await frame.cleanup(); - }) describe('Sentinel Client', function () { let sentinel: RedisSentinelType | undefined; beforeEach(async function () { - this.timeout(0); - + this.timeout(15000); + + last = Date.now(); + + function deltaMeasurer() { + const delta = Date.now() - last; + if (delta > longestDelta) { + longestDelta = delta; + } + if (delta > longestTestDelta) { + longestTestDelta = delta; + } + if (!stopMeasuringBlocking) { + last = Date.now(); + setImmediate(deltaMeasurer); + } + } + setImmediate(deltaMeasurer); + await frame.spawnRedisSentinel(); await frame.getAllRunning(); await steadyState(frame); longestTestDelta = 0; @@ -522,6 +506,10 @@ describe.skip('legacy tests', () => { await sentinel.destroy(); sentinel = undefined; } + + stopMeasuringBlocking = true; + + await frame.cleanup(); }) it('use', async function () { @@ -863,7 +851,6 @@ describe.skip('legacy tests', () => { it('shutdown sentinel node', async function () { this.timeout(60000); - sentinel = frame.getSentinelClient(); sentinel.setTracer(tracer); sentinel.on("error", () => { }); @@ -1020,7 +1007,7 @@ describe.skip('legacy tests', () => { this.timeout(30000); const csc = new BasicPooledClientSideCache(); - sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5}); + sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3 as const}, RESP: 3 as const, clientSideCache: csc, masterPoolSize: 5}); await sentinel.connect(); await sentinel.set('x', 1); diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 86bc5b3178..60c1a59689 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -4,12 +4,13 @@ import { once } from 'node:events'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { RedisSentinelOptions, RedisSentinelType } from './types'; -import RedisClient from '../client'; +import RedisClient, {RedisClientType} from '../client'; import RedisSentinel from '.'; import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; const execAsync = promisify(exec); import RedisSentinelModule from './module' - +import TestUtils from '@redis/test-utils'; +import { DEBUG_MODE_ARGS } from '../test-utils' interface ErrorWithCode extends Error { code: string; } @@ -125,7 +126,6 @@ export interface RedisSentinelConfig { sentinelServerArgument?: Array sentinelName: string; - sentinelQuorum?: number; password?: string; } @@ -151,6 +151,7 @@ export interface SentinelController { } export class SentinelFramework extends DockerBase { + #testUtils: TestUtils; #nodeList: Awaited> = []; /* port -> docker info/client */ #nodeMap: Map>>>; @@ -170,7 +171,11 @@ export class SentinelFramework extends DockerBase { super(); this.config = config; - + this.#testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' + }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); } @@ -190,7 +195,7 @@ export class SentinelFramework extends DockerBase { const options: RedisSentinelOptions = { ...opts, name: this.config.sentinelName, - sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.docker.port } }), + sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.port } }), passthroughClientErrorEvents: errors } @@ -218,11 +223,11 @@ export class SentinelFramework extends DockerBase { throw new Error("inconsistent state with partial setup"); } - this.#nodeList = await this.spawnRedisSentinelNodes(); - this.#nodeList.map((value) => this.#nodeMap.set(value.docker.port.toString(), value)); + this.#nodeList = await this.spawnRedisSentinelNodes(2); + this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value)); - this.#sentinelList = await this.spawnRedisSentinelSentinels(); - this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); + this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3) + this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value)); this.#spawned = true; } @@ -234,11 +239,8 @@ export class SentinelFramework extends DockerBase { return Promise.all( [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( - async ({ docker, client }) => { - if (client.isOpen) { - client.destroy(); - } - this.dockerRemove(docker.dockerId); + async ({ dockerId }) => { + this.dockerRemove(dockerId); } ) ).finally(async () => { @@ -248,112 +250,33 @@ export class SentinelFramework extends DockerBase { }); } - protected async spawnRedisSentinelNodeDocker() { - const imageInfo: RedisServerDockerConfig = this.config.nodeDockerConfig ?? { image: "redis/redis-stack-server", version: "latest" }; - const serverArguments: Array = this.config.nodeServerArguments ?? []; - let environment; - if (this.config.password !== undefined) { - environment = `REDIS_ARGS="{port} --requirepass ${this.config.password}"`; - } else { - environment = 'REDIS_ARGS="{port}"'; - } - - const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments, environment); - const client = await RedisClient.create({ - password: this.config.password, - socket: { - port: docker.port - } - }).on("error", () => { }).connect(); - - return { - docker, - client - }; - } - - protected async spawnRedisSentinelNodes() { - const master = await this.spawnRedisSentinelNodeDocker(); + protected async spawnRedisSentinelNodes(replicasCount: number) { + const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + + const replicas: Array = [] + for (let i = 0; i < replicasCount; i++) { + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + replicas.push(replica) - const promises: Array> = []; + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) - for (let i = 0; i < (this.config.numberOfNodes ?? 0) - 1; i++) { - promises.push( - this.spawnRedisSentinelNodeDocker().then(async node => { - if (this.config.password !== undefined) { - await node.client.configSet({'masterauth': this.config.password}) - } - await node.client.replicaOf('127.0.0.1', master.docker.port); - return node; - }) - ); + await client.connect(); + await client.replicaOf("127.0.0.1", master.port); + await client.close(); } return [ master, - ...await Promise.all(promises) - ]; - } - - protected async spawnRedisSentinelSentinelDocker() { - const imageInfo: RedisServerDockerConfig = this.config.sentinelDockerConfig ?? { image: "redis", version: "latest" } - let serverArguments: Array; - if (this.config.password === undefined) { - serverArguments = this.config.sentinelServerArgument ?? - [ - "/bin/bash", - "-c", - "\"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} \"" - ]; - } else { - serverArguments = this.config.sentinelServerArgument ?? - [ - "/bin/bash", - "-c", - `"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} --requirepass ${this.config.password}"` - ]; - } - - const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments); - const client = await RedisClient.create({ - modules: RedisSentinelModule, - password: this.config.password, - socket: { - port: docker.port - } - }).on("error", () => { }).connect(); - - return { - docker, - client - }; + ...replicas + ] } - protected async spawnRedisSentinelSentinels() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - - const promises: Array> = []; - - for (let i = 0; i < (this.config.numberOfSentinels ?? 3); i++) { - promises.push( - this.spawnRedisSentinelSentinelDocker().then(async sentinel => { - await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); - const options: Array<{option: RedisArgument, value: RedisArgument}> = []; - options.push({ option: "down-after-milliseconds", value: "100" }); - options.push({ option: "failover-timeout", value: "5000" }); - if (this.config.password !== undefined) { - options.push({ option: "auth-pass", value: this.config.password }); - } - await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options) - return sentinel; - }) - ); - } - - return [ - ...await Promise.all(promises) - ] + protected async spawnRedisSentinelSentinels(masterPort: number, sentinels: number) { + return this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, masterPort, this.config.sentinelName, sentinels) } async getAllRunning() { @@ -384,90 +307,71 @@ export class SentinelFramework extends DockerBase { } async addSentinel() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - const sentinel = await this.spawnRedisSentinelSentinelDocker(); - - await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); - const options: Array<{option: RedisArgument, value: RedisArgument}> = []; - options.push({ option: "down-after-milliseconds", value: "100" }); - options.push({ option: "failover-timeout", value: "5000" }); - if (this.config.password !== undefined) { - options.push({ option: "auth-pass", value: this.config.password }); - } - await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options); - - this.#sentinelList.push(sentinel); - this.#sentinelMap.set(sentinel.docker.port.toString(), sentinel); + const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1) + this.#sentinelList.push(nodes[0]); + this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]); } async addNode() { const masterPort = await this.getMasterPort(); - const newNode = await this.spawnRedisSentinelNodeDocker(); + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) - if (this.config.password !== undefined) { - await newNode.client.configSet({'masterauth': this.config.password}) - } - await newNode.client.replicaOf('127.0.0.1', masterPort); + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) + + await client.connect(); + await client.replicaOf("127.0.0.1", masterPort); + await client.close(); + - this.#nodeList.push(newNode); - this.#nodeMap.set(newNode.docker.port.toString(), newNode); + this.#nodeList.push(replica); + this.#nodeMap.set(replica.port.toString(), replica); } async getMaster(tracer?: Array): Promise { - for (const sentinel of this.#sentinelMap!.values()) { - let info; - - try { - if (!sentinel.client.isReady) { - continue; - } - - info = await sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - if (tracer) { - tracer.push('getMaster: master data returned from sentinel'); - tracer.push(JSON.stringify(info, undefined, '\t')) - } - } catch (err) { - console.log("getMaster: sentinelMaster call failed: " + err); - continue; - } - - const master = this.#nodeMap.get(info.port); - if (master === undefined) { - throw new Error(`couldn't find master node for ${info.port}`); - } - - if (tracer) { - tracer.push(`getMaster: master port is either ${info.port} or ${master.docker.port}`); - } + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const info = await client.sentinel.sentinelMaster(this.config.sentinelName); + await client.close() - if (!master.client.isOpen) { - throw new Error(`Sentinel's expected master node (${info.port}) is now down`); - } + const master = this.#nodeMap.get(info.port); + if (master === undefined) { + throw new Error(`couldn't find master node for ${info.port}`); + } - return info.port; + if (tracer) { + tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`); } - throw new Error("Couldn't get master"); + return info.port; } async getMasterPort(tracer?: Array): Promise { const data = await this.getMaster(tracer) - return this.#nodeMap.get(data!)!.docker.port; + return this.#nodeMap.get(data!)!.port; } getRandomNode() { - return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].docker.port.toString(); + return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].port.toString(); } async getRandonNonMasterNode(): Promise { const masterPort = await this.getMasterPort(); while (true) { const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)]; - if (node.docker.port != masterPort) { - return node.docker.port.toString(); + if (node.port != masterPort) { + return node.port.toString(); } } } @@ -479,11 +383,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - if (node.client.isOpen) { - node.client.destroy(); - } - - return await this.dockerStop(node.docker.dockerId); + return await this.dockerStop(node.dockerId); } async restartNode(id: string) { @@ -492,15 +392,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - await this.dockerStart(node.docker.dockerId); - if (!node.client.isOpen) { - node.client = await RedisClient.create({ - password: this.config.password, - socket: { - port: node.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(node.dockerId); } async stopSentinel(id: string) { @@ -509,11 +401,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - if (sentinel.client.isOpen) { - sentinel.client.destroy(); - } - - return await this.dockerStop(sentinel.docker.dockerId); + return await this.dockerStop(sentinel.dockerId); } async restartSentinel(id: string) { @@ -522,16 +410,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - await this.dockerStart(sentinel.docker.dockerId); - if (!sentinel.client.isOpen) { - sentinel.client = await RedisClient.create({ - modules: RedisSentinelModule, - password: this.config.password, - socket: { - port: sentinel.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(sentinel.dockerId); } getNodePort(id: string) { @@ -540,13 +419,13 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - return node.docker.port; + return node.port; } getAllNodesPort() { let ports: Array = []; for (const node of this.#nodeList) { - ports.push(node.docker.port); + ports.push(node.port); } return ports @@ -555,7 +434,7 @@ export class SentinelFramework extends DockerBase { getAllDockerIds() { let ids = new Map(); for (const node of this.#nodeList) { - ids.set(node.docker.dockerId, node.docker.port); + ids.set(node.dockerId, node.port); } return ids; @@ -567,43 +446,67 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - return sentinel.docker.port; + return sentinel.port; } getAllSentinelsPort() { let ports: Array = []; for (const sentinel of this.#sentinelList) { - ports.push(sentinel.docker.port); + ports.push(sentinel.port); } return ports } getSetinel(i: number): string { - return this.#sentinelList[i].docker.port.toString(); + return this.#sentinelList[i].port.toString(); } - sentinelSentinels() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); - } - } + async sentinelSentinels() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const sentinels = client.sentinel.sentinelSentinels(this.config.sentinelName) + await client.close() + + return sentinels } - sentinelMaster() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - } - } + async sentinelMaster() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const master = client.sentinel.sentinelMaster(this.config.sentinelName) + await client.close() + + return master } - sentinelReplicas() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); - } - } + async sentinelReplicas() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const replicas = client.sentinel.sentinelReplicas(this.config.sentinelName) + await client.close() + + return replicas } } \ No newline at end of file diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 63f43ba5e6..19bbafc66e 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -14,7 +14,7 @@ const utils = TestUtils.createFromConfig({ export default utils; -const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? +export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index 3814a80923..47257964f6 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -62,7 +62,7 @@ export interface RedisServerDocker { dockerId: string; } -async function spawnRedisServerDocker( +export async function spawnRedisServerDocker( options: RedisServerDockerOptions, serverArguments: Array): Promise { let port; if (options.mode == "sentinel") { @@ -374,35 +374,16 @@ export async function spawnRedisSentinel( const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); for (let i = 0; i < sentinelCount; i++) { - sentinelPromises.push((async () => { - const port = (await portIterator.next()).value; - - let sentinelConfig = `port ${port} -sentinel monitor mymaster 127.0.0.1 ${master.port} 2 -sentinel down-after-milliseconds mymaster 5000 -sentinel failover-timeout mymaster 6000 -`; - if (password !== undefined) { - sentinelConfig += `requirepass ${password}\n`; - sentinelConfig += `sentinel auth-pass mymaster ${password}\n`; - } - - const dir = fs.mkdtempSync(path.join(tmpDir, i.toString())); - fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => { - if (err) { - console.error("failed to create temporary config file", err); - } - }); - - return await spawnRedisServerDocker( - { - image: dockerConfigs.image, - version: dockerConfigs.version, - mode: "sentinel", - mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], - port: port, - }, serverArguments); - })()); + sentinelPromises.push( + spawnSentinelNode( + dockerConfigs, + serverArguments, + master.port, + "mymaster", + path.join(tmpDir, i.toString()), + password, + ), + ) } const sentinelNodes = await Promise.all(sentinelPromises); @@ -424,3 +405,43 @@ after(() => { }) ); }); + + +export async function spawnSentinelNode( + dockerConfigs: RedisServerDockerOptions, + serverArguments: Array, + masterPort: number, + sentinelName: string, + tmpDir: string, + password?: string, +) { + const port = (await portIterator.next()).value; + + let sentinelConfig = `port ${port} +sentinel monitor ${sentinelName} 127.0.0.1 ${masterPort} 2 +sentinel down-after-milliseconds ${sentinelName} 500 +sentinel failover-timeout ${sentinelName} 1000 +`; + if (password !== undefined) { + sentinelConfig += `requirepass ${password}\n`; + sentinelConfig += `sentinel auth-pass ${sentinelName} ${password}\n`; + } + + const dir = fs.mkdtempSync(tmpDir); + fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => { + if (err) { + console.error("failed to create temporary config file", err); + } + }); + + return await spawnRedisServerDocker( + { + image: dockerConfigs.image, + version: dockerConfigs.version, + mode: "sentinel", + mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], + port: port, + }, + serverArguments, + ); +} \ No newline at end of file diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index d92c5c9e3d..a41f970e0c 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -19,10 +19,13 @@ import { RedisClusterType } from '@redis/client/index'; import { RedisNode } from '@redis/client/lib/sentinel/types' -import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; +import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions, RedisServerDocker, spawnSentinelNode, spawnRedisServerDocker } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; interface TestUtilsConfig { /** @@ -395,19 +398,19 @@ export default class TestUtils { S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} ->( - range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), - title: string, - fn: (sentinel: RedisSentinelType) => unknown, - options: SentinelTestOptions -): void { - - if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { - return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) - } else { - console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) + >( + range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), + title: string, + fn: (sentinel: RedisSentinelType) => unknown, + options: SentinelTestOptions + ): void { + + if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { + return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) + } else { + console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) + } } -} testWithClientPool< M extends RedisModules = {}, @@ -541,4 +544,46 @@ export default class TestUtils { this.testWithClient(`client.${title}`, fn, options.client); this.testWithCluster(`cluster.${title}`, fn, options.cluster); } + + + spawnRedisServer< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions + ): Promise { + return spawnRedisServerDocker(this.#DOCKER_IMAGE, options.serverArguments) + } + + async spawnRedisSentinels< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions, + masterPort: number, + sentinelName: string, + count: number + ): Promise> { + const sentinels: Array = []; + for (let i = 0; i < count; i++) { + const appPrefix = 'sentinel-config-dir'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); + + sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir)) + + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true }); + } + } + + return sentinels + } } From 708b22f366d2825f5b2b58c49a764373e6814bfa Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 28 May 2025 13:15:37 +0530 Subject: [PATCH 06/19] docs(readme): replace relative GitHub links with absolute URLs (#2980) --- packages/redis/README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/redis/README.md b/packages/redis/README.md index d04a19b0d7..ab6b4707e6 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -45,13 +45,13 @@ npm install redis | Name | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------- | -| [`redis`](../redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | -| [`@redis/client`](../client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | -| [`@redis/bloom`](../bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | -| [`@redis/json`](../json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | -| [`@redis/search`](../search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | -| [`@redis/time-series`](../time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | -| [`@redis/entraid`](../entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | +| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | > Looking for a high-level library to handle object mapping? > See [redis-om-node](https://github.com/redis/redis-om-node)! @@ -83,7 +83,7 @@ createClient({ ``` You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in -the [client configuration guide](../../docs/client-configuration.md). +the [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md). To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it @@ -188,7 +188,7 @@ await pool.ping(); ### Pub/Sub -See the [Pub/Sub overview](../../docs/pub-sub.md). +See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md). ### Scan Iterator @@ -250,7 +250,7 @@ const client = createClient({ }); ``` -See the [V5 documentation](../../docs/v5.md#client-side-caching) for more details and advanced usage. +See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining @@ -274,11 +274,11 @@ await Promise.all([ ### Programmability -See the [Programmability overview](../../docs/programmability.md). +See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md). ### Clustering -Check out the [Clustering Guide](../../docs/clustering.md) when using Node Redis to connect to a Redis Cluster. +Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster. ### Events @@ -291,12 +291,12 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | | `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | | `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | -| `sharded-channel-moved` | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | +| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | > :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and > an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. -> The client will not emit [any other events](../../docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. +> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions @@ -313,13 +313,13 @@ Node Redis is supported with the following versions of Redis: ## Migration -- [From V3 to V4](../../docs/v3-to-v4.md) -- [From V4 to V5](../../docs/v4-to-v5.md) -- [V5](../../docs/v5.md) +- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md) +- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md) +- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md) ## Contributing -If you'd like to contribute, check out the [contributing guide](../../CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md). Thank you to all the people who already contributed to Node Redis! @@ -327,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis! ## License -This repository is licensed under the "MIT" license. See [LICENSE](../../LICENSE). +This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE). From 4b939a7bdbc5791e1b33212da1890994c36ff5e7 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:16:25 +0300 Subject: [PATCH 07/19] Release client@5.1.1 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index dd41dc53e0..a6d44451a6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From bd6f764d91ee46d833a21c0671e8bc79693fcfa3 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:19:30 +0300 Subject: [PATCH 08/19] Updated the Bloom package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/bloom/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab4cbe138b..9ffa7854a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/client": { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/client": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", + "integrity": "sha512-FMD35y2KgCWTBLOfF0MhwDSaIVcu5mOUuTV9Kw3JOWHMgON3+ulht31cjTB/gph0BfD1vzUvCeROeRaf/d+35w==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.0", diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 3c8e37c5b7..e642650c28 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 3b03ced4ea5e7e8c316a9d38e607c91aa044d664 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:20:11 +0300 Subject: [PATCH 09/19] Release bloom@5.1.1 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index e642650c28..547e5ee64e 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 566b16eb53b8d362dd910228832bac1f01df931c Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:21:25 +0300 Subject: [PATCH 10/19] Updated the Entraid package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/entraid/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ffa7854a8..d08701f4df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/entraid/node_modules/@types/node": { @@ -8632,6 +8632,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/bloom": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.0.tgz", + "integrity": "sha512-Gp5RWvVKbvItMU2sd848yhY/BnigToz8H4PYcvlBBSP5cQ3lVP1LMh5Kx2CYBNzCdDabVicwBKNvaoLBqPNqIg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/redis/node_modules/@redis/client": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", diff --git a/packages/entraid/package.json b/packages/entraid/package.json index ba44351b3b..bd20b01ac7 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -21,7 +21,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@types/express": "^4.17.21", From e4d903ca0cbe17577a3a06f9f54ffddd993c0630 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:21:59 +0300 Subject: [PATCH 11/19] Release entraid@5.1.1 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index bd20b01ac7..43f26d1e03 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From e68b5deb190f9ab841ca2f987b2e5f5483096188 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:23:12 +0300 Subject: [PATCH 12/19] Updated the Json package to use client@5.1.1 --- package-lock.json | 4 ++-- packages/json/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d08701f4df..21eeebac22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8615,7 +8615,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/redis": { diff --git a/packages/json/package.json b/packages/json/package.json index fd0b3c768f..b3fe4a27ee 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 8c7b384e6b51e0b74d19267a0c94e6e68baca952 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:23:31 +0300 Subject: [PATCH 13/19] Release json@5.1.1 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index b3fe4a27ee..3b473dfce0 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From b2213d813254b842f3b0f424b74a416fedd6178f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:24:32 +0300 Subject: [PATCH 14/19] Updated the Search package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/search/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21eeebac22..c33dc30b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8656,6 +8656,18 @@ "node": ">= 18" } }, + "packages/redis/node_modules/@redis/json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.0.tgz", + "integrity": "sha512-laXZt1Rlimk3py5ZoABBnd4xn/8dWbLUWGvVS7avgMhdczS+eWtXpElilJbFpc+7ZpVlol4vaSGFuR8ThDcTFw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/search": { "name": "@redis/search", "version": "5.1.0", @@ -8667,7 +8679,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/test-utils": { diff --git a/packages/search/package.json b/packages/search/package.json index ac56b5ff5d..a44f6322a7 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -13,7 +13,7 @@ "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From 13566cf9d6a85ce076d8bd4df735353bb71a7686 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:24:51 +0300 Subject: [PATCH 15/19] Release search@5.1.1 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index a44f6322a7..7cb73dfc0a 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From d178b7839a458bc7c3e20569a416e9d4c7a4d4cc Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:25:37 +0300 Subject: [PATCH 16/19] Updated the Timeseries package to use client@5.1.1 --- package-lock.json | 16 ++++++++++++++-- packages/time-series/package.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c33dc30b28..325eefb08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8668,9 +8668,21 @@ "@redis/client": "^5.1.0" } }, + "packages/redis/node_modules/@redis/search": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.0.tgz", + "integrity": "sha512-afQYMeIdWGNPjr84GxxgJVkolxMW3BBrlNOwhRHPdzbdGh0rTUPjOJpGHBig34ostHX6AhZ6QwqceU1zLluDEg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.1.0" + } + }, "packages/search": { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8757,7 +8769,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 466c53df22..3acef8b866 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" From d4380221c84a83734c2936ce3fc624c7b962b123 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:25:56 +0300 Subject: [PATCH 17/19] Release time-series@5.1.1 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 3acef8b866..888f0f317e 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From d374514309a4975367dfbf02d14538c721cd2f34 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:27:23 +0300 Subject: [PATCH 18/19] Updated the Redis package to use client@5.1.1 --- package-lock.json | 60 ++++--------------------------------- packages/redis/package.json | 10 +++---- 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 325eefb08b..8fae6bef85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8622,64 +8622,16 @@ "version": "5.1.0", "license": "MIT", "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" } }, - "packages/redis/node_modules/@redis/bloom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.1.0.tgz", - "integrity": "sha512-Gp5RWvVKbvItMU2sd848yhY/BnigToz8H4PYcvlBBSP5cQ3lVP1LMh5Kx2CYBNzCdDabVicwBKNvaoLBqPNqIg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, - "packages/redis/node_modules/@redis/client": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.1.0.tgz", - "integrity": "sha512-FMD35y2KgCWTBLOfF0MhwDSaIVcu5mOUuTV9Kw3JOWHMgON3+ulht31cjTB/gph0BfD1vzUvCeROeRaf/d+35w==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/redis/node_modules/@redis/json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.1.0.tgz", - "integrity": "sha512-laXZt1Rlimk3py5ZoABBnd4xn/8dWbLUWGvVS7avgMhdczS+eWtXpElilJbFpc+7ZpVlol4vaSGFuR8ThDcTFw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, - "packages/redis/node_modules/@redis/search": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.1.0.tgz", - "integrity": "sha512-afQYMeIdWGNPjr84GxxgJVkolxMW3BBrlNOwhRHPdzbdGh0rTUPjOJpGHBig34ostHX6AhZ6QwqceU1zLluDEg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.1.0" - } - }, "packages/search": { "name": "@redis/search", "version": "5.1.1", @@ -8760,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/redis/package.json b/packages/redis/package.json index b6c3f409a6..e9e863cc26 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" From e4a1ca467fcbc1942fa6eb2f7b60b081110544e4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 28 May 2025 16:31:12 +0300 Subject: [PATCH 19/19] Release redis@5.1.1 --- package-lock.json | 2 +- packages/redis/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fae6bef85..6d2460ed19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8619,7 +8619,7 @@ } }, "packages/redis": { - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@redis/bloom": "5.1.1", diff --git a/packages/redis/package.json b/packages/redis/package.json index e9e863cc26..05fb70cdd1 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts",