From 9e727ce20722b94f1891d8a44382845278ac0928 Mon Sep 17 00:00:00 2001 From: Stef Schoonderwoerd Date: Tue, 9 Sep 2025 16:01:56 +0200 Subject: [PATCH 1/3] add emitInvalidate option --- packages/client/lib/client/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 57b1231670..fedc9a3fcc 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -144,6 +144,11 @@ export interface RedisClientOptions< * Tag to append to library name that is sent to the Redis server */ clientInfoTag?: string; + /** + * When set to true, client tracking is turned on and the client emits `invalidate` events when it receives invalidation messages from the redis server. + * Mutually exclusive with `clientSideCache` option. + */ + emitInvalidate?: boolean; } export type WithCommands< @@ -465,6 +470,8 @@ export default class RedisClient< this.#clientSideCache = new BasicClientSideCache(cscConfig); } this.#queue.setInvalidateCallback(this.#clientSideCache.invalidate.bind(this.#clientSideCache)); + } else if (options?.emitInvalidate) { + this.#queue.setInvalidateCallback((key) => this.emit('invalidate', key)); } } @@ -472,8 +479,11 @@ export default class RedisClient< if (options?.clientSideCache && options?.RESP !== 3) { throw new Error('Client Side Caching is only supported with RESP3'); } - + if (options?.clientSideCache && options?.emitInvalidate) { + throw new Error('emitInvalidate is not supported (or necessary) when clientSideCache is enabled'); + } } + #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { // Convert username/password to credentialsProvider if no credentialsProvider is already in place @@ -674,11 +684,14 @@ export default class RedisClient< } }); } - + if (this.#clientSideCache) { commands.push({cmd: this.#clientSideCache.trackingOn()}); } + if (this.#options?.emitInvalidate) { + commands.push({cmd: ['CLIENT', 'TRACKING', 'ON']}); + } return commands; } From 98959f27310d33e92316774d0a08bba10cc4e4b0 Mon Sep 17 00:00:00 2001 From: Stef Schoonderwoerd Date: Wed, 10 Sep 2025 13:39:32 +0200 Subject: [PATCH 2/3] Add documentation for event --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1789a51d37..e6332764e4 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,7 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `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](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) | +| `invalidate` | Client Tracking is on with `emitInvalidate` and a key is invalidated | `(key: RedisItem \| null)` | > :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. From cb54d4d2386117a2271fe3da67961ad92c854653 Mon Sep 17 00:00:00 2001 From: Stef Schoonderwoerd Date: Thu, 11 Sep 2025 07:49:11 +0200 Subject: [PATCH 3/3] Re-write emitInvalidate logic --- packages/client/lib/client/index.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index fc250814ea..fada269302 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -530,6 +530,19 @@ export default class RedisClient< this.#clientSideCache?.invalidate(null) } + return true + }); + } else if (options?.emitInvalidate) { + this.#queue.addPushHandler((push: Array): boolean => { + if (push[0].toString() !== 'invalidate') return false; + + if (push[1] !== null) { + for (const key of push[1]) { + this.emit('invalidate', key); + } + } else { + this.emit('invalidate', null); + } return true }); } @@ -539,14 +552,15 @@ export default class RedisClient< if (options?.clientSideCache && options?.RESP !== 3) { throw new Error('Client Side Caching is only supported with RESP3'); } + if (options?.emitInvalidate && options?.RESP !== 3) { + throw new Error('emitInvalidate is only supported with RESP3'); + } if (options?.clientSideCache && options?.emitInvalidate) { throw new Error('emitInvalidate is not supported (or necessary) when clientSideCache is enabled'); + } if (options?.maintPushNotifications && options?.maintPushNotifications !== 'disabled' && options?.RESP !== 3) { throw new Error('Graceful Maintenance is only supported with RESP3'); - } - - } - + } } #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { @@ -756,6 +770,10 @@ export default class RedisClient< commands.push({cmd: this.#clientSideCache.trackingOn()}); } + if (this.#options?.emitInvalidate) { + commands.push({cmd: ['CLIENT', 'TRACKING', 'ON']}); + } + const { tls, host } = this.#options!.socket as RedisTcpSocketOptions; const maintenanceHandshakeCmd = await EnterpriseMaintenanceManager.getHandshakeCommand(!!tls, host!, this.#options!); if(maintenanceHandshakeCmd) {