@@ -54,55 +54,65 @@ export function getRedisConnectionDefaults(
5454 }
5555}
5656
57- let globalRedisClient : Redis | null = null
58- let pingFailures = 0
59- let pingInterval : NodeJS . Timeout | null = null
60- let pingInFlight = false
57+ interface RedisState {
58+ client : Redis | null
59+ pingFailures : number
60+ pingInterval : NodeJS . Timeout | null
61+ pingInFlight : boolean
62+ reconnectListeners : Array < ( ) => void >
63+ }
64+
65+ const g = globalThis as typeof globalThis & { _redisState ?: RedisState }
66+ if ( ! g . _redisState ) {
67+ g . _redisState = {
68+ client : null ,
69+ pingFailures : 0 ,
70+ pingInterval : null ,
71+ pingInFlight : false ,
72+ reconnectListeners : [ ] ,
73+ }
74+ }
75+ const state = g . _redisState
6176
6277const PING_INTERVAL_MS = 15_000
6378const MAX_PING_FAILURES = 2
6479
65- /** Callbacks invoked when the PING health check forces a reconnect. */
66- const reconnectListeners : Array < ( ) => void > = [ ]
67-
6880/**
6981 * Register a callback that fires when the PING health check forces a reconnect.
7082 * Useful for resetting cached adapters that hold a stale Redis reference.
7183 */
7284export function onRedisReconnect ( cb : ( ) => void ) : void {
73- reconnectListeners . push ( cb )
85+ state . reconnectListeners . push ( cb )
7486}
7587
7688function startPingHealthCheck ( redis : Redis ) : void {
77- if ( pingInterval ) return
89+ if ( state . pingInterval ) return
7890
79- pingInterval = setInterval ( async ( ) => {
80- if ( pingInFlight ) return
81- pingInFlight = true
91+ state . pingInterval = setInterval ( async ( ) => {
92+ if ( state . pingInFlight ) return
93+ state . pingInFlight = true
8294 try {
8395 await redis . ping ( )
84- pingFailures = 0
96+ state . pingFailures = 0
8597 } catch ( error ) {
86- pingFailures ++
98+ state . pingFailures ++
8799 logger . warn ( 'Redis PING failed' , {
88- consecutiveFailures : pingFailures ,
100+ consecutiveFailures : state . pingFailures ,
89101 error : toError ( error ) . message ,
90102 } )
91103
92- if ( pingFailures >= MAX_PING_FAILURES ) {
104+ if ( state . pingFailures >= MAX_PING_FAILURES ) {
93105 logger . error ( 'Redis PING failed consecutive times — forcing reconnect' , {
94- consecutiveFailures : pingFailures ,
106+ consecutiveFailures : state . pingFailures ,
95107 } )
96- pingFailures = 0
97- // Drop the cached client and stop this health check before disconnecting,
98- // so the next getRedisClient() builds a fresh client and a fresh PING loop.
99- // Listeners may call getRedisClient() and must observe the cleared global.
100- globalRedisClient = null
101- if ( pingInterval ) {
102- clearInterval ( pingInterval )
103- pingInterval = null
108+ state . pingFailures = 0
109+ // Clear before notifying listeners — they may call getRedisClient() and must see the reset state.
110+ state . client = null
111+ if ( state . pingInterval ) {
112+ clearInterval ( state . pingInterval )
113+ state . pingInterval = null
104114 }
105- for ( const cb of reconnectListeners ) {
115+ for ( const cb of state . reconnectListeners ) {
106116 try {
107117 cb ( )
108118 } catch ( cbError ) {
@@ -116,7 +126,7 @@ function startPingHealthCheck(redis: Redis): void {
116126 }
117127 }
118128 } finally {
119- pingInFlight = false
129+ state . pingInFlight = false
120130 }
121131 } , PING_INTERVAL_MS )
122132}
@@ -131,15 +141,15 @@ function startPingHealthCheck(redis: Redis): void {
131141export function getRedisClient ( ) : Redis | null {
132142 if ( typeof window !== 'undefined' ) return null
133143 if ( ! redisUrl ) return null
134- if ( globalRedisClient ) return globalRedisClient
144+ if ( state . client ) return state . client
135145
136146 // Outside the try/catch so config errors aren't silently swallowed.
137147 const defaults = getRedisConnectionDefaults ( redisUrl )
138148
139149 try {
140150 logger . info ( 'Initializing Redis client' )
141151
142- globalRedisClient = new Redis ( redisUrl , {
152+ state . client = new Redis ( redisUrl , {
143153 ...defaults ,
144154 commandTimeout : 5000 ,
145155 maxRetriesPerRequest : 5 ,
@@ -162,17 +172,17 @@ export function getRedisClient(): Redis | null {
162172 } ,
163173 } )
164174
165- globalRedisClient . on ( 'connect' , ( ) => logger . info ( 'Redis connected' ) )
166- globalRedisClient . on ( 'ready' , ( ) => logger . info ( 'Redis ready' ) )
167- globalRedisClient . on ( 'error' , ( err : Error ) => {
175+ state . client . on ( 'connect' , ( ) => logger . info ( 'Redis connected' ) )
176+ state . client . on ( 'ready' , ( ) => logger . info ( 'Redis ready' ) )
177+ state . client . on ( 'error' , ( err : Error ) => {
168178 logger . error ( 'Redis error' , { error : err . message , code : ( err as any ) . code } )
169179 } )
170- globalRedisClient . on ( 'close' , ( ) => logger . warn ( 'Redis connection closed' ) )
171- globalRedisClient . on ( 'end' , ( ) => logger . error ( 'Redis connection ended' ) )
180+ state . client . on ( 'close' , ( ) => logger . warn ( 'Redis connection closed' ) )
181+ state . client . on ( 'end' , ( ) => logger . error ( 'Redis connection ended' ) )
172182
173- startPingHealthCheck ( globalRedisClient )
183+ startPingHealthCheck ( state . client )
174184
175- return globalRedisClient
185+ return state . client
176186 } catch ( error ) {
177187 logger . error ( 'Failed to initialize Redis client' , { error } )
178188 return null
@@ -274,18 +284,18 @@ export async function extendLock(
274284 * Use for graceful shutdown.
275285 */
276286export async function closeRedisConnection ( ) : Promise < void > {
277- if ( pingInterval ) {
278- clearInterval ( pingInterval )
279- pingInterval = null
287+ if ( state . pingInterval ) {
288+ clearInterval ( state . pingInterval )
289+ state . pingInterval = null
280290 }
281291
282- if ( globalRedisClient ) {
292+ if ( state . client ) {
283293 try {
284- await globalRedisClient . quit ( )
294+ await state . client . quit ( )
285295 } catch ( error ) {
286296 logger . error ( 'Error closing Redis connection' , { error } )
287297 } finally {
288- globalRedisClient = null
298+ state . client = null
289299 }
290300 }
291301}
@@ -294,12 +304,12 @@ export async function closeRedisConnection(): Promise<void> {
294304 * Reset all module-level state. Only intended for use in tests.
295305 */
296306export function resetForTesting ( ) : void {
297- if ( pingInterval ) {
298- clearInterval ( pingInterval )
299- pingInterval = null
307+ if ( state . pingInterval ) {
308+ clearInterval ( state . pingInterval )
309+ state . pingInterval = null
300310 }
301- globalRedisClient = null
302- pingFailures = 0
303- pingInFlight = false
304- reconnectListeners . length = 0
311+ state . client = null
312+ state . pingFailures = 0
313+ state . pingInFlight = false
314+ state . reconnectListeners . length = 0
305315}
0 commit comments