11import { Request , Response , NextFunction } from "express" ;
2- import { redis } from "../config/redis.conifg " ;
2+ import { redis } from "../config/redis.config " ;
33import logger from "../config/logger.config" ;
44import { EventEmitter } from "events" ;
5+ import LRUCache from "lru-cache" ;
56
67// Singleflight tracker for cache stampede protection
78const inFlightRequests = new Map < string , EventEmitter > ( ) ;
89
9- // L1 In-Memory Cache (Node.js Heap)
10- const l1Cache = new Map < string , { data : any , expiresAt : number } > ( ) ;
10+ // Bounded L1 in-memory cache. Capped at 5000 entries; entries older than
11+ // 5 minutes are evicted on read. This prevents the previous unbounded
12+ // Map that could OOM the process under diverse URL load.
13+ const L1_MAX_ENTRIES = 5000 ;
14+ const L1_TTL_MS = 5 * 60 * 1000 ;
15+ const l1Cache = new LRUCache < string , { data : any , expiresAt : number } > ( {
16+ max : L1_MAX_ENTRIES ,
17+ ttl : L1_TTL_MS ,
18+ ttlAutopurge : true ,
19+ } ) ;
1120
1221// Subscribe to invalidation events from the Leaderboard service
1322const subscriber = redis . duplicate ( ) ;
@@ -56,7 +65,7 @@ export const cacheMiddleware = (ttlSeconds: number) => {
5665 if ( cachedResponse ) {
5766 // Populate L1 cache on L2 hit
5867 l1Cache . set ( cacheKey , { data : cachedResponse , expiresAt : now + ttlSeconds * 1000 } ) ;
59-
68+
6069 res . setHeader ( "X-Cache" , "L2-HIT" ) ;
6170 res . setHeader ( "Content-Type" , "application/json" ) ;
6271 return res . send ( cachedResponse ) ;
@@ -66,14 +75,14 @@ export const cacheMiddleware = (ttlSeconds: number) => {
6675 if ( inFlightRequests . has ( cacheKey ) ) {
6776 res . setHeader ( "X-Cache" , "COALESCED" ) ;
6877 res . setHeader ( "Content-Type" , "application/json" ) ;
69-
78+
7079 const emitter = inFlightRequests . get ( cacheKey ) ! ;
7180 return await new Promise < void > ( ( resolve ) => {
7281 emitter . once ( "done" , ( body : any ) => {
7382 res . send ( body ) ;
7483 resolve ( ) ;
7584 } ) ;
76-
85+
7786 emitter . once ( "error" , ( ) => {
7887 res . status ( 503 ) . json ( { success : false , message : "Service Unavailable" } ) ;
7988 resolve ( ) ;
@@ -82,7 +91,7 @@ export const cacheMiddleware = (ttlSeconds: number) => {
8291 }
8392
8493 const emitter = new EventEmitter ( ) ;
85- emitter . setMaxListeners ( 1000 ) ;
94+ emitter . setMaxListeners ( 1000 ) ;
8695 inFlightRequests . set ( cacheKey , emitter ) ;
8796
8897 res . setHeader ( "X-Cache" , "MISS" ) ;
@@ -94,15 +103,15 @@ export const cacheMiddleware = (ttlSeconds: number) => {
94103 redis . setex ( cacheKey , ttlSeconds , body ) . catch ( err => {
95104 logger . error ( "Failed to set L2 cache:" , { error : err . message , key : cacheKey } ) ;
96105 } ) ;
97-
106+
98107 // Populate L1
99108 l1Cache . set ( cacheKey , { data : body , expiresAt : Date . now ( ) + ttlSeconds * 1000 } ) ;
100-
109+
101110 emitter . emit ( "done" , body ) ;
102111 } else {
103112 emitter . emit ( "error" ) ;
104113 }
105-
114+
106115 inFlightRequests . delete ( cacheKey ) ;
107116 return originalSend . call ( this , body ) ;
108117 } ;
@@ -115,3 +124,8 @@ export const cacheMiddleware = (ttlSeconds: number) => {
115124 } ;
116125} ;
117126
127+ export function shutdownCacheMiddleware ( ) {
128+ if ( subscriber && subscriber . status === 'ready' ) {
129+ subscriber . quit ( ) . catch ( ( ) => undefined ) ;
130+ }
131+ }
0 commit comments