1- import { Redis } from "ioredis " ;
1+ import { createClient , RedisClientType } from "@redis/client " ;
22import { DefaultAzureCredential } from "@azure/identity" ;
3- import { EntraIdManagedIdentityAuthenticator } from "@redis/entraid" ;
3+ import { EntraIdCredentialsProviderFactory , REDIS_SCOPE_DEFAULT } from "@redis/entraid" ;
44
55/**
66 * A service for interacting with a Redis database. In local development, this will be implemented as an in-memory database. For non-local environments, it will be implemented as a Redis database.
77 */
8- export interface DatabaseService {
8+ export interface RedisDatabaseService {
99 ready : Promise < void > ;
1010 getJson < T > ( key : string ) : Promise < T | null > ;
1111 save < T > ( key : string , value : T ) : Promise < void > ;
@@ -18,11 +18,10 @@ export interface DatabaseService {
1818/**
1919 * Database service that connects to PWABuilder's Redis instance in Azure.
2020 */
21- export class RedisService implements DatabaseService {
21+ export class RedisService implements RedisDatabaseService {
2222 public readonly ready : Promise < void > ;
23- private readonly redis : Redis ;
23+ private readonly redis : RedisClientType ;
2424 private readonly redisHost : string ;
25- private readonly authenticator : EntraIdManagedIdentityAuthenticator ;
2625
2726 /**
2827 *
@@ -33,30 +32,38 @@ export class RedisService implements DatabaseService {
3332 throw new Error ( "REDIS_HOST environment variable is not set." ) ;
3433 }
3534
36- this . authenticator = new EntraIdManagedIdentityAuthenticator ( new DefaultAzureCredential ( ) ) ;
3735 this . redis = this . createRedisConnection ( ) ;
3836 this . ready = this . initializeConnection ( ) ;
3937 }
4038
41- private createRedisConnection ( ) : Redis {
42- return new Redis ( {
43- host : this . redisHost ,
44- port : 6380 ,
45- tls : {
46- servername : this . redisHost
47- } ,
48- connectTimeout : 30000 , // 30 seconds to establish connection (Azure cold start)
49- commandTimeout : 10000 , // 10 seconds for individual commands
50- lazyConnect : true , // Don't connect immediately
51- maxRetriesPerRequest : 3 , // Reasonable retries
52- enableReadyCheck : false , // Disable ready check - authenticate first
53- keepAlive : 30000 , // Keep connections alive
54- family : 4 , // Force IPv4 for better Azure compatibility
55- reconnectOnError : ( err ) => {
56- const targetErrors = [ 'READONLY' , 'ECONNRESET' , 'ETIMEDOUT' , 'ENOTFOUND' , 'ECONNREFUSED' ] ;
57- return targetErrors . some ( targetError => err . message . includes ( targetError ) ) ;
39+ private createRedisConnection ( ) : RedisClientType {
40+ // Create credentials provider for authentication
41+ const credential = new DefaultAzureCredential ( ) ;
42+ const credentialsProvider = EntraIdCredentialsProviderFactory . createForDefaultAzureCredential ( {
43+ credential,
44+ scopes : REDIS_SCOPE_DEFAULT ,
45+ tokenManagerConfig : {
46+ expirationRefreshRatio : 0.8 // Refresh token after 80% of its lifetime
5847 }
5948 } ) ;
49+
50+ return createClient ( {
51+ socket : {
52+ host : this . redisHost ,
53+ port : 6380 ,
54+ tls : true ,
55+ connectTimeout : 30000 , // 30 seconds to establish connection (Azure cold start)
56+ reconnectStrategy : ( retries ) => {
57+ // Exponential backoff with max 3000ms delay
58+ const delay = Math . min ( retries * 100 , 3000 ) ;
59+ console . info ( `Reconnecting to Redis, attempt ${ retries } , waiting ${ delay } ms` ) ;
60+ return delay ;
61+ }
62+ } ,
63+ credentialsProvider,
64+ commandsQueueMaxLength : 1000 ,
65+ pingInterval : 30000 // Keep-alive ping every 30 seconds
66+ } ) ;
6067 }
6168
6269 private async initializeConnection ( ) : Promise < void > {
@@ -66,35 +73,13 @@ export class RedisService implements DatabaseService {
6673 console . error ( "Redis connection error:" , err ) ;
6774 } ) ;
6875
69- this . redis . on ( "close" , ( ) => {
70- console . warn ( "Redis connection closed" ) ;
71- } ) ;
72-
73- // Connect to Redis
74- await this . redis . connect ( ) ;
75- console . info ( "Redis connected, authenticating with managed identity..." ) ;
76-
77- // Authenticate using EntraId authenticator (handles token refresh automatically)
78- await this . authenticator . authenticateWithHello ( this . redis ) ;
79- console . info ( "Redis authenticated successfully with managed identity" ) ;
80-
81- // Set up reconnection handler to re-authenticate
8276 this . redis . on ( "reconnecting" , ( ) => {
8377 console . info ( "Redis reconnecting..." ) ;
8478 } ) ;
8579
86- // Re-authenticate after reconnection
87- this . redis . on ( "connect" , async ( ) => {
88- if ( this . redis . status === "reconnecting" || this . redis . status === "connecting" ) {
89- console . info ( "Re-authenticating after reconnection..." ) ;
90- try {
91- await this . authenticator . authenticateWithHello ( this . redis ) ;
92- console . info ( "Re-authentication successful" ) ;
93- } catch ( authError ) {
94- console . error ( "Redis re-authentication failed:" , authError ) ;
95- }
96- }
97- } ) ;
80+ // Connect to Redis - authentication is handled automatically by credentialsProvider
81+ await this . redis . connect ( ) ;
82+ console . info ( "Redis connected and authenticated with managed identity" ) ;
9883 } catch ( error ) {
9984 console . error ( "Failed to initialize Redis connection:" , error ) ;
10085 throw error ;
@@ -141,7 +126,9 @@ export class RedisService implements DatabaseService {
141126 } ) ;
142127
143128 await Promise . race ( [
144- this . redis . set ( key , JSON . stringify ( value ) , 'EX' , expirationSeconds ) ,
129+ this . redis . set ( key , JSON . stringify ( value ) , {
130+ EX : expirationSeconds
131+ } ) ,
145132 timeoutPromise
146133 ] ) ;
147134
@@ -162,9 +149,8 @@ export class RedisService implements DatabaseService {
162149 const startTime = Date . now ( ) ;
163150
164151 // Check Redis status first
165- const redisStatus = this . redis . status ;
166- if ( redisStatus !== 'ready' ) {
167- console . warn ( `Redis status is ${ redisStatus } , skipping dequeue for key: ${ key } ` ) ;
152+ if ( ! this . redis . isReady ) {
153+ console . warn ( 'Redis not ready, skipping dequeue for key:' , key ) ;
168154 return null ;
169155 }
170156
@@ -173,7 +159,7 @@ export class RedisService implements DatabaseService {
173159 } ) ;
174160
175161 const json = await Promise . race ( [
176- this . redis . lpop ( key ) ,
162+ this . redis . lPop ( key ) ,
177163 timeoutPromise
178164 ] ) ;
179165
@@ -194,8 +180,7 @@ export class RedisService implements DatabaseService {
194180 // If there's a connection error, provide diagnostics but don't try to reconnect here
195181 // Let the Redis client handle reconnection automatically
196182 try {
197- const redisStatus = this . redis . status ;
198- console . error ( `Redis status after dequeue failure: ${ redisStatus } ` ) ;
183+ console . error ( 'Redis ready status after dequeue failure:' , this . redis . isReady ) ;
199184 } catch ( diagError ) {
200185 console . error ( 'Failed to get Redis status:' , diagError ) ;
201186 }
@@ -208,7 +193,7 @@ export class RedisService implements DatabaseService {
208193 * @param value The value to push onto the list. This will be converted to a JSON string.
209194 */
210195 async enqueue < T > ( key : string , value : T ) : Promise < number > {
211- const totalItems = await this . redis . rpush ( key , JSON . stringify ( value ) ) ;
196+ const totalItems = await this . redis . rPush ( key , JSON . stringify ( value ) ) ;
212197 console . info ( `Added ${ key } to the queue ${ key } . Queue length is now ${ totalItems } ` ) ;
213198 return totalItems ;
214199 }
@@ -219,7 +204,7 @@ export class RedisService implements DatabaseService {
219204 * @returns The length of the list.
220205 */
221206 queueLength ( key : string ) : Promise < number > {
222- return this . redis . llen ( key ) ;
207+ return this . redis . lLen ( key ) ;
223208 }
224209
225210 /**
@@ -228,7 +213,7 @@ export class RedisService implements DatabaseService {
228213 */
229214 async healthCheck ( ) : Promise < boolean > {
230215 try {
231- if ( this . redis . status !== 'ready' ) {
216+ if ( ! this . redis . isReady ) {
232217 return false ;
233218 }
234219
@@ -247,7 +232,7 @@ export class RedisService implements DatabaseService {
247232 }
248233}
249234
250- class InMemoryDatabaseService implements DatabaseService {
235+ class InMemoryDatabaseService implements RedisDatabaseService {
251236 private readonly store : Map < string , string > = new Map ( ) ; // key is ID of the object, value is the JSON string.
252237 private readonly queues : Map < string , any [ ] > = new Map ( ) ; // key is the name of the queue, value is an array of JSON strings representing the items in the queue.
253238 public readonly ready = Promise . resolve ( ) ;
@@ -303,5 +288,5 @@ if (isLocalDev) {
303288 console . info ( `${ process . env . NODE_ENV } environment detected.Using Redis for database service.` ) ;
304289}
305290export const database = isLocalDev ?
306- new InMemoryDatabaseService ( ) as DatabaseService :
307- new RedisService ( ) as DatabaseService ;
291+ new InMemoryDatabaseService ( ) as RedisDatabaseService :
292+ new RedisService ( ) as RedisDatabaseService ;
0 commit comments