Conversation
Summary of ChangesHello @Antignote, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a valuable feature by allowing an existing Redis client to be passed to the RedisCacheHandler. This enhances flexibility and promotes connection reuse. The implementation is clean, and the conditional attachment of the error listener is a good practice. The documentation updates are also clear and helpful.
However, I've identified a significant issue with the connection lifecycle management. The close() method will unconditionally close the Redis connection, even if it's a shared client provided externally. This could lead to unexpected disconnections in other parts of an application. My review includes a detailed comment on how to address this by tracking whether the client was created internally.
|
@mrjasonroy did you have any chance to have a look at this? |
mrjasonroy
left a comment
There was a problem hiding this comment.
Review: feat/add support for redis client
Good feature direction — passing an existing client is a real need for connection sharing, Sentinel/Cluster setups, and test mocking. The implementation is close but has two critical bugs and no tests.
Verdict: Request Changes
Critical
instanceof Redisis fragile and will silently fail across package versions/bundlers (see inline)- The two
instanceofchecks at lines 108 and 127 can disagree, creating a worst-case state where a new phantom connection is opened AND no error listener is attached
Blocking
- Zero tests — no
handlers/redis.test.tsexists at all. This PR adds lifecycle-sensitive behavior (conditionalquit(), conditional error listener) with no coverage.
Questions for the author
- Why
instanceof Redisinstead of a duck-type check or theRedisClientinterface pattern used indata-cache/redis.ts? Deliberate divergence? - Is there a plan to add tests before merge?
- Should
close()be added to theCacheHandlerinterface? The behavior now differs depending on client ownership — composability matters.
| constructor(options: RedisCacheHandlerOptions = {}) { | ||
| // Initialize Redis connection | ||
| if (typeof options.redis === "string") { | ||
| if (options.redis instanceof Redis) { |
There was a problem hiding this comment.
🔴 Critical: instanceof Redis is fragile and will silently break across package versions.
instanceof fails when the consumer's ioredis version differs from the one bundled here, or when bundlers (Webpack, esbuild) duplicate the module. When it fails, the code falls through to the else branch — calling new Redis(options.redis) with your client object as config. ioredis silently ignores unknown props and opens a new default connection to localhost:6379. No error thrown. You'll just be talking to the wrong Redis.
The sibling data-cache/redis.ts already solves this with a RedisClient interface (duck-typing). Recommend the same approach here:
function isRedisClient(value: unknown): value is Redis {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Redis).get === 'function' &&
typeof (value as Redis).set === 'function' &&
typeof (value as Redis).quit === 'function'
);
}| console.error("[RedisCacheHandler] Redis connection error:", err); | ||
| }); | ||
| // Handle Redis connection errors (only add listener if we created the client) | ||
| if (!(options.redis instanceof Redis)) { |
There was a problem hiding this comment.
🔴 This check can disagree with line 108, creating a worst-case scenario.
If instanceof is unreliable (see line 108 comment), both checks can fail simultaneously — meaning: the code creates a new client (line 108 falls through to else) AND skips the error listener here. That's a phantom connection with unhandled errors.
Use this.didCreateClient instead — it's already tracking this and is guaranteed to agree with the actual branching logic:
if (this.didCreateClient) {
this.redis.on('error', (err) => { ... });
}This is simpler, correct, and consistent with how close() at line 315 already does it.
| private readonly tagPrefix: string; | ||
| private readonly defaultTTL?: number; | ||
| private readonly debug: boolean; | ||
| private didCreateClient = false; |
There was a problem hiding this comment.
🟡 Nit: should be readonly.
This flag is set once in the constructor and never mutated after. Mark it readonly for clarity and to let the type system enforce that:
private readonly didCreateClient: boolean;Then assign it explicitly in each constructor branch (e.g. this.didCreateClient = true / false).
| // Handle Redis connection errors (only add listener if we created the client) | ||
| if (!(options.redis instanceof Redis)) { | ||
| this.redis.on("error", (err) => { | ||
| console.error("[RedisCacheHandler] Redis connection error:", err); |
There was a problem hiding this comment.
🟡 Foot-gun: if the consumer also forgot to add an error listener, Node.js will crash.
Multiple 'error' listeners on the same EventEmitter are fine — they're called in order. A safer approach: always attach one, but check first so we don't add a duplicate logger:
if (this.redis.listenerCount('error') === 0) {
this.redis.on('error', (err) => {
console.error('[RedisCacheHandler] Redis connection error:', err);
});
}At minimum, the JSDoc for the shared-client option should document that the caller is responsible for error handling.
mrjasonroy
left a comment
There was a problem hiding this comment.
Thanks for this contribution, @Antignote! Accepting an existing Redis client instance is a great feature for connection reuse and advanced configs (Cluster, Sentinel, etc.). The didCreateClient lifecycle pattern is well done.
A few things to address before we can merge:
1. instanceof Redis fragility across package boundaries
If the user's app has a different ioredis version than this package's dependency, instanceof will return false (different constructors from different node_modules copies). A duck-typing check would be more robust:
function isRedisInstance(value: unknown): value is Redis {
return (
typeof value === "object" &&
value !== null &&
typeof (value as Redis).get === "function" &&
typeof (value as Redis).set === "function" &&
typeof (value as Redis).del === "function"
);
}Or at minimum, document in the JSDoc that the ioredis version must match.
2. Inconsistent condition for error listener
The error listener guard (line ~125) duplicates the instanceof check:
if (!(options.redis instanceof Redis)) {
this.redis.on("error", ...);
}This should use the didCreateClient flag you already have, for consistency and to avoid the instanceof issue:
if (this.didCreateClient) {
this.redis.on("error", (err) => { ... });
}3. No tests
Would like to see at least:
- Passing an existing client instance and verifying get/set works
close()does not callquit()on a shared clientclose()does callquit()on an internally-created client
4. Needs rebase
This branch is from Jan 14 and main has had 14+ commits since. Will need a rebase to merge cleanly.
Overall the approach is solid — just needs these cleanups. Let us know if you'd like help with any of them!
Allows RedisCacheHandler to accept a pre-existing ioredis client instance, enabling connection reuse and custom configurations (Cluster, Sentinel, etc.). Key changes: - Accept Redis instance via duck typing (avoids instanceof fragility) - Track didCreateClient to skip error listeners for shared clients - close() only disconnects internally-created clients - Added 5 tests for existing client behavior Based on #23 by @Antignote with improvements to the detection mechanism and lifecycle management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds support for existing Redis client to ISR cache handler
- Replace instanceof Redis with duck typing (avoids fragility across package versions and bundler setups) - Use didCreateClient consistently for error listener check - Add 5 tests for existing client behavior (lifecycle, error listeners) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3990b0d to
2185376
Compare
|
Rebased onto main and pushed a fix commit addressing the review feedback:
All 95 tests pass. Ready to merge once CI is green. |
Adds support for existing Redis client to ISR cache handler
Description
This PR enhances the
RedisCacheHandlerto accept an existing Redis client instance, providing more flexibility for applications that need to share a Redis connection or have custom Redis configurations.Changes
Updated
RedisCacheHandlerOptions.redisto accept:Redisclient instance (ioredis)RedisOptionsobject (existing)Improved connection lifecycle management: When an existing client is passed, the handler skips adding error listeners to avoid conflicts with the application's own error handling
Enhanced documentation with examples showing both usage patterns:
Benefits
Example Usage
Type of Change
Testing
pnpm test)pnpm test:e2e)Checklist
pnpm lintpasses)pnpm formatto format my codepnpm typecheck)Related Issues
N/A
Additional Context