1- import ioredis , { Pipeline } from 'ioredis' ;
1+ // Dynamically require ioredis to prevent strict TypeScript binding
2+ // and handle module export differences between v4 and v5.
3+ let RedisConstructor : any ;
4+ try {
5+ const ioredisLib = require ( 'ioredis' ) ;
6+ RedisConstructor = ioredisLib . default || ioredisLib ;
7+ } catch ( e ) {
8+ // If we reach here, the peer dependency is missing
9+ throw new Error ( 'ioredis is missing. Please install ioredis v4 or v5.' ) ;
10+ }
11+
212import { ILogger } from '../../logger/types' ;
313import { merge , isString } from '../../utils/lang' ;
414import { thenable } from '../../utils/promise/thenable' ;
@@ -20,42 +30,70 @@ const DEFAULT_OPTIONS = {
2030const DEFAULT_LIBRARY_OPTIONS = {
2131 enableOfflineQueue : false ,
2232 connectTimeout : DEFAULT_OPTIONS . connectionTimeout ,
23- lazyConnect : false
33+ lazyConnect : false ,
34+ // CRITICAL: v5 defaults this to 0 (disabled), which breaks dynamic clusters.
35+ // v4 defaulted to 5000. We explicitly set it here to ensure v5 works like v4.
36+ slotsRefreshInterval : 5000 ,
2437} ;
2538
2639interface IRedisCommand {
27- resolve : ( ) => void ,
40+ resolve : ( value ?: any ) => void ,
2841 reject : ( err ?: any ) => void ,
29- command : ( ) => Promise < void > ,
30- name : string
42+ command : ( ) => Promise < any > ,
43+ name : string ,
3144}
3245
3346/**
3447 * Redis adapter on top of the library of choice (written with ioredis) for some extra control.
48+ * Refactored to use Composition and Proxy instead of Inheritance to support both v4 and v5.
3549 */
36- export class RedisAdapter extends ioredis {
50+ export class RedisAdapter {
51+ // eslint-disable-next-line no-undef -- Index signature to allow proxying dynamic ioredis methods without TS errors
52+ [ key : string ] : any ;
3753 private readonly log : ILogger ;
38- private _options : object ;
54+ private _options : Record < string , any > ;
3955 private _notReadyCommandsQueue ?: IRedisCommand [ ] ;
4056 private _runningCommands : Set < Promise < any > > ;
4157
58+ // The actual ioredis instance
59+ public client : any ;
60+
4261 constructor ( log : ILogger , storageSettings : Record < string , any > = { } ) {
4362 const options = RedisAdapter . _defineOptions ( storageSettings ) ;
44- // Call the ioredis constructor
45- // @ts -ignore
46- super ( ...RedisAdapter . _defineLibrarySettings ( options ) ) ;
4763
4864 this . log = log ;
4965 this . _options = options ;
5066 this . _notReadyCommandsQueue = [ ] ;
5167 this . _runningCommands = new Set ( ) ;
68+
69+ // Instantiate the client using the dynamic constructor
70+ const librarySettings = RedisAdapter . _defineLibrarySettings ( options ) ;
71+ this . client = new RedisConstructor ( ...librarySettings ) ;
72+
5273 this . _listenToEvents ( ) ;
5374 this . _setTimeoutWrappers ( ) ;
5475 this . _setDisconnectWrapper ( ) ;
76+
77+ // Return a Proxy. This allows the adapter to act exactly like an extended class.
78+ // If a method/property is accessed that we didn't explicitly wrap, it forwards it to `this.client`.
79+ return new Proxy ( this , {
80+ get ( target : RedisAdapter , prop : string | symbol ) {
81+ // If the property exists on our wrapper (like wrapped 'get', 'set', or internal methods)
82+ if ( prop in target ) {
83+ return target [ prop as keyof RedisAdapter ] ;
84+ }
85+ // If it doesn't exist on our wrapper but exists on the real client (like 'on', 'quit')
86+ if ( target . client && prop in target . client ) {
87+ const val = target . client [ prop ] ;
88+ return typeof val === 'function' ? val . bind ( target . client ) : val ;
89+ }
90+ return undefined ;
91+ }
92+ } ) ;
5593 }
5694
5795 _listenToEvents ( ) {
58- this . once ( 'ready' , ( ) => {
96+ this . client . once ( 'ready' , ( ) => {
5997 const commandsCount = this . _notReadyCommandsQueue ? this . _notReadyCommandsQueue . length : 0 ;
6098 this . log . info ( LOG_PREFIX + `Redis connection established. Queued commands: ${ commandsCount } .` ) ;
6199
@@ -66,24 +104,21 @@ export class RedisAdapter extends ioredis {
66104 // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them.
67105 this . _notReadyCommandsQueue = undefined ;
68106 } ) ;
69- this . once ( 'close' , ( ) => {
107+ this . client . once ( 'close' , ( ) => {
70108 this . log . info ( LOG_PREFIX + 'Redis connection closed.' ) ;
71109 } ) ;
72110 }
73111
74112 _setTimeoutWrappers ( ) {
75- const instance : Record < string , any > = this ;
76-
77- const wrapCommand = ( originalMethod : Function , methodName : string ) => {
78- // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one)
79- // or it can be the instance of a Pipeline object.
80- return function ( this : RedisAdapter | Pipeline ) {
81- const params = arguments ;
82- const caller = this ;
113+ const instance = this ;
83114
115+ // We pass `bindTarget` so pipeline execution is bound to the pipeline object,
116+ // while standard commands are bound to the client.
117+ const wrapCommand = ( originalMethod : Function , methodName : string , bindTarget : any ) => {
118+ return function ( ...params : any [ ] ) {
84119 function commandWrapper ( ) {
85120 instance . log . debug ( `${ LOG_PREFIX } Executing ${ methodName } .` ) ;
86- const result = originalMethod . apply ( caller , params ) ;
121+ const result = originalMethod . apply ( bindTarget , params ) ;
87122
88123 if ( thenable ( result ) ) {
89124 // For handling pending commands on disconnect, add to the set and remove once finished.
@@ -107,7 +142,7 @@ export class RedisAdapter extends ioredis {
107142
108143 if ( instance . _notReadyCommandsQueue ) {
109144 return new Promise ( ( resolve , reject ) => {
110- instance . _notReadyCommandsQueue . unshift ( {
145+ instance . _notReadyCommandsQueue ! . unshift ( {
111146 resolve,
112147 reject,
113148 command : commandWrapper ,
@@ -122,19 +157,19 @@ export class RedisAdapter extends ioredis {
122157
123158 // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
124159 METHODS_TO_PROMISE_WRAP . forEach ( methodName => {
125- const originalFn = instance [ methodName ] ;
126- instance [ methodName ] = wrapCommand ( originalFn , methodName ) ;
160+ const originalFn = this . client [ methodName ] ;
161+ this [ methodName ] = wrapCommand ( originalFn , methodName , this . client ) ;
127162 } ) ;
128163
129164 // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
130165 METHODS_TO_PROMISE_WRAP_EXEC . forEach ( methodName => {
131- const originalFn = instance [ methodName ] ;
166+ const originalFn = this . client [ methodName ] ;
132167 // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
133- instance [ methodName ] = function ( ) {
134- const res = originalFn . apply ( instance , arguments ) ;
168+ this [ methodName ] = function ( ... args : any [ ] ) {
169+ const res = originalFn . apply ( instance . client , args ) ;
135170 const originalExec = res . exec ;
136171
137- res . exec = wrapCommand ( originalExec , methodName + ' .exec' ) . bind ( res ) ;
172+ res . exec = wrapCommand ( originalExec , ` ${ methodName } .exec` , res ) ;
138173
139174 return res ;
140175 } ;
@@ -143,27 +178,26 @@ export class RedisAdapter extends ioredis {
143178
144179 _setDisconnectWrapper ( ) {
145180 const instance = this ;
146- const originalMethod = instance . disconnect ;
147-
148- instance . disconnect = function disconnect ( ...params : [ ] ) {
181+ const originalMethod = this . client . disconnect ;
149182
183+ this . disconnect = function disconnect ( ...params : any [ ] ) {
150184 setTimeout ( function deferredDisconnect ( ) {
151185 if ( instance . _runningCommands . size > 0 ) {
152186 instance . log . info ( LOG_PREFIX + `Attempting to disconnect but there are ${ instance . _runningCommands . size } commands still waiting for resolution. Defering disconnection until those finish.` ) ;
153187
154188 Promise . all ( setToArray ( instance . _runningCommands ) )
155189 . then ( ( ) => {
156190 instance . log . debug ( LOG_PREFIX + 'Pending commands finished successfully, disconnecting.' ) ;
157- originalMethod . apply ( instance , params ) ;
191+ originalMethod . apply ( instance . client , params ) ;
158192 } )
159193 . catch ( e => {
160194 instance . log . warn ( LOG_PREFIX + `Pending commands finished with error: ${ e } . Proceeding with disconnection.` ) ;
161- originalMethod . apply ( instance , params ) ;
195+ originalMethod . apply ( instance . client , params ) ;
162196 } ) ;
163197 } else {
164198 instance . log . debug ( LOG_PREFIX + 'No commands pending execution, disconnect.' ) ;
165199 // Nothing pending, just proceed.
166- originalMethod . apply ( instance , params ) ;
200+ originalMethod . apply ( instance . client , params ) ;
167201 }
168202 } , 10 ) ;
169203 } ;
0 commit comments