Skip to content

fix: preserve subclasses in duplicate() (#2053)#2098

Open
rit3sh-x wants to merge 6 commits into
redis:mainfrom
rit3sh-x:fix/duplicate-respects-subclass
Open

fix: preserve subclasses in duplicate() (#2053)#2098
rit3sh-x wants to merge 6 commits into
redis:mainfrom
rit3sh-x:fix/duplicate-respects-subclass

Conversation

@rit3sh-x

@rit3sh-x rit3sh-x commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Closes #2053.

Summary

Redis#duplicate() and Cluster#duplicate() now preserve the actual class of the instance, so subclasses are returned from duplicate() instead of plain Redis / Cluster.

Root cause

The applyMixin utility in lib/utils/applyMixin.ts copied every own property from the mixin prototype onto the derived class prototype — including the built-in constructor property. When applyMixin(Redis, EventEmitter) runs at module load, EventEmitter.prototype.constructor overwrites Redis.prototype.constructor, so any Redis instance had r.constructor === EventEmitter instead of Redis.

That is why a naive new this.constructor(...) fix for duplicate() previously broke Cluster (as noted in Pavel's comment on #2053) — the constructor was wrong at the root.

Fix

  1. lib/utils/applyMixin.ts — skip the constructor key when copying descriptors, using the canonical Object.getOwnPropertyDescriptors + delete + Object.defineProperties pattern. Preserves getters/setters, non-enumerable props, and symbol keys.
  2. lib/Redis.tsduplicate() now uses this.constructor and returns this.
  3. lib/cluster/index.ts — same treatment for Cluster.duplicate(), typed with ClusterNode[] / ClusterOptions for proper type safety.

Example

class InstrumentedRedis extends Redis {
  getTag() { return "instrumented"; }
}

const redis = new InstrumentedRedis(6379);
const dup = redis.duplicate();
// Before: dup instanceof InstrumentedRedis === false (plain Redis)
// After:  dup instanceof InstrumentedRedis === true
// After:  dup.getTag() === "instrumented"

Test plan

New unit test file test/unit/duplicate.ts with 15 tests covering:

  • Redis.prototype.constructor correctly resolves to Redis (applyMixin fix)
  • Cluster.prototype.constructor correctly resolves to Cluster
  • redis.duplicate() returns a Redis instance on plain Redis
  • redis.duplicate() returns a subclass instance when called on a subclass
  • Subclass methods are preserved across duplicate()
  • Subclass instance fields (set in constructor) are preserved
  • Override options are applied to the duplicate
  • Deeply-nested subclass hierarchies (A → B → C → Redis) work
  • Same coverage for Cluster subclasses
  • applyMixin still mixes in EventEmitter methods (on, emit, removeAllListeners) onto both Redis and Cluster

Verification

  • npm run build — PASS
  • npm run test:tsd — PASS
  • Full unit + functional suite: 609 passing, 26 pending, only pre-existing flakes unrelated to this change
  • Cluster integration tests (Docker): 17/17 passing
  • New duplicate tests: 15/15 passing

Relation to #2073

Independent but complementary. #2073 proposes a redisClass option on Cluster to thread a custom class through all internal connections. This PR fixes the underlying inheritance bug that prevented new this.constructor(...) from working in the first place.

Either PR can land first — there are no file-level conflicts (different line ranges in cluster/index.ts).


Note

Low Risk
Targeted inheritance/mixin fix with broad unit tests; duplicate() is used internally (e.g. MONITOR, slot refresh) but behavior aligns with documented intent.

Overview
Fixes #2053 so duplicate() on custom Redis / Cluster subclasses returns the same class (instrumentation, extra methods, constructor fields) instead of a plain base instance.

Root cause: applyMixin copied EventEmitter.prototype.constructor onto Redis/Cluster, so instance.constructor was wrong and duplicate() could not safely use new this.constructor(...).

Changes: applyMixin now skips copying constructor when merging prototypes. Redis#duplicate() and Cluster#duplicate() instantiate via this.constructor and are typed to return this. New unit tests cover constructors, nested subclasses, overrides, and that EventEmitter APIs still work.

Reviewed by Cursor Bugbot for commit 994ec8c. Bugbot is set up for automated code reviews on this repo. Configure here.

  Fix applyMixin copying EventEmitter.prototype.constructor onto the
  derived prototype, which caused instance.constructor to resolve to
  EventEmitter instead of Redis/Cluster.

  With applyMixin corrected to skip the constructor property, Redis and
  Cluster duplicate() methods can safely use  so
  subclass instances are preserved across duplicate() calls.

  This enables legitimate subclassing patterns (instrumentation,
  OpenTelemetry, custom error handling) to survive through duplicate()
  without monkeypatching.

  Closes redis#2053
@PavelPashov

Copy link
Copy Markdown
Contributor

Thanks for the PR. I will review it and follow up once I have gone through it.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@PavelPashov any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

duplicate method should new subclasses

2 participants