cache.js
1 import { EventEmitter } from "events"; 2 import { logger } from "@/middleware/logging.js"; 3 import config from "@/config.js"; 4 5 class CacheEventEmitter extends EventEmitter { 6 constructor() { 7 super(); 8 this.setMaxListeners(100); 9 } 10 } 11 12 const cacheEvents = new CacheEventEmitter(); 13 14 export const CacheEvents = { 15 USER_BANNED: "user:banned", 16 USER_UNBANNED: "user:unbanned", 17 USER_UPDATED: "user:updated", 18 SESSION_INVALIDATED: "session:invalidated", 19 TOKEN_REVOKED: "token:revoked", 20 PASSWORD_CHANGED: "password:changed", 21 SECURITY_SETTINGS_CHANGED: "security:changed", 22 LOGIN_ATTEMPT: "auth:login_attempt", 23 LOGIN_FAILED: "auth:login_failed", 24 LOGIN_SUCCESS: "auth:login_success", 25 ACCOUNT_LOCKED: "account:locked", 26 ACCOUNT_UNLOCKED: "account:unlocked", 27 TRANSACTION_CREATED: "transaction:created", 28 TRANSACTION_COMPLETED: "transaction:completed", 29 TRANSACTION_FAILED: "transaction:failed", 30 TRANSACTION_CANCELLED: "transaction:cancelled", 31 WITHDRAWAL_LOCKED: "withdrawal:locked", 32 WITHDRAWAL_RELEASED: "withdrawal:released", 33 WITHDRAWAL_FAILED: "withdrawal:failed", 34 DEPOSIT_DETECTED: "deposit:detected", 35 DEPOSIT_CONFIRMED: "deposit:confirmed", 36 DEPOSIT_CREDITED: "deposit:credited", 37 DEPOSIT_MONITOR_STARTED: "deposit:monitor_started", 38 DEPOSIT_MONITOR_STOPPED: "deposit:monitor_stopped", 39 BALANCE_UPDATED: "balance:updated", 40 BALANCE_SNAPSHOT: "balance:snapshot", 41 NETWORK_CHANGED: "network:changed", 42 RPC_PROVIDER_CHANGED: "rpc:provider_changed", 43 BLOCK_RECEIVED: "block:received", 44 WEBSOCKET_CONNECTED: "ws:connected", 45 WEBSOCKET_DISCONNECTED: "ws:disconnected", 46 SUBSCRIPTION_CREATED: "ws:subscription", 47 SUBSCRIPTION_REMOVED: "ws:unsubscription", 48 SWEEPER_STARTED: "sweeper:started", 49 SWEEPER_COMPLETED: "sweeper:completed", 50 SWEEPER_FAILED: "sweeper:failed", 51 SERVICE_HEALTH_CHECK: "system:health_check", 52 CONFIG_UPDATED: "system:config_updated", 53 SERVICE_STARTED: "service:started", 54 SERVICE_STOPPED: "service:stopped", 55 SUSPICIOUS_ACTIVITY: "security:suspicious", 56 RATE_LIMIT_EXCEEDED: "security:rate_limit", 57 RATE_LIMIT_TRIGGERED: "rate:triggered", 58 RATE_LIMIT_RESET: "rate:reset", 59 THROTTLE_ACTIVATED: "throttle:activated", 60 IP_BLOCKED: "security:ip_blocked", 61 IP_UNBLOCKED: "security:ip_unblocked", 62 PROXY_ROTATED: "proxy:rotated", 63 PROXY_FAILED: "proxy:failed", 64 PROXY_BLACKLISTED: "proxy:blacklisted", 65 TOR_CIRCUIT_CHANGED: "tor:circuit_changed", 66 TOR_CONNECTED: "tor:connected", 67 TOR_DISCONNECTED: "tor:disconnected", 68 CACHE_CLEARED: "cache:cleared", 69 CACHE_WARMED: "cache:warmed", 70 CACHE_INVALIDATED: "cache:invalidated", 71 CACHE_SIZE_LIMIT: "cache:size_limit", 72 RATE_UPDATED: "rate:updated", 73 PRICE_FETCHED: "price:fetched", 74 PRICE_FAILED: "price:failed", 75 EXCHANGE_RATE_CHANGED: "exchange:rate_changed", 76 WALLET_CONNECTED: "wallet:connected", 77 WALLET_DISCONNECTED: "wallet:disconnected", 78 WALLET_SIGNATURE_VERIFIED: "wallet:signature_verified", 79 WALLET_AUTH_FAILED: "wallet:auth_failed", 80 ERROR_OCCURRED: "error:occurred", 81 ERROR_RECOVERED: "error:recovered", 82 RETRY_ATTEMPTED: "retry:attempted", 83 RETRY_FAILED: "retry:failed", 84 DATA_BACKUP: "data:backup", 85 DATA_RESTORED: "data:restored", 86 DATA_MIGRATED: "data:migrated", 87 DATA_VALIDATED: "data:validated", 88 FALLBACK_ACTIVATED: "fallback:activated", 89 FALLBACK_RECOVERED: "fallback:recovered", 90 DEPOSIT_DETECTED: "deposit:detected", 91 DEPOSIT_ERROR: "deposit:error", 92 WEBSOCKET_STATE_CHANGED: "websocket:state_changed", 93 }; 94 95 class UnifiedCacheManager { 96 constructor() { 97 this.caches = new Map(); 98 this.cacheStats = new Map(); 99 this.globalTTL = config.timeouts.veryLong; 100 this.maxCacheSize = 10000; 101 this.cleanupInterval = config.timeouts.long; 102 this.setupEventListeners(); 103 this.startCleanup(); 104 } 105 106 createCache(name, options = {}) { 107 const cache = { 108 store: new Map(), 109 ttl: options.ttl || this.globalTTL, 110 maxSize: options.maxSize || this.maxCacheSize, 111 stats: { 112 hits: 0, 113 misses: 0, 114 sets: 0, 115 deletes: 0, 116 evictions: 0, 117 }, 118 }; 119 120 this.caches.set(name, cache); 121 this.cacheStats.set(name, cache.stats); 122 123 logger.debug(`Created cache: ${name}`); 124 return cache; 125 } 126 127 get(cacheName, key) { 128 const cache = this.caches.get(cacheName); 129 if (!cache) { 130 logger.warn(`Cache not found: ${cacheName}`); 131 return null; 132 } 133 134 const item = cache.store.get(key); 135 if (!item) { 136 cache.stats.misses++; 137 return null; 138 } 139 140 if (Date.now() > item.expiresAt) { 141 cache.store.delete(key); 142 cache.stats.misses++; 143 return null; 144 } 145 146 cache.stats.hits++; 147 item.lastAccessed = Date.now(); 148 return item.value; 149 } 150 151 set(cacheName, key, value, customTTL) { 152 const cache = this.caches.get(cacheName); 153 if (!cache) { 154 logger.warn(`Cache not found: ${cacheName}`); 155 return false; 156 } 157 158 const ttl = customTTL || cache.ttl; 159 const expiresAt = Date.now() + ttl; 160 161 if (cache.store.size >= cache.maxSize && !cache.store.has(key)) { 162 this.evictLRU(cache); 163 cache.stats.evictions++; 164 } 165 166 cache.store.set(key, { 167 value, 168 expiresAt, 169 createdAt: Date.now(), 170 lastAccessed: Date.now(), 171 }); 172 173 cache.stats.sets++; 174 return true; 175 } 176 177 delete(cacheName, key) { 178 const cache = this.caches.get(cacheName); 179 if (!cache) { 180 return false; 181 } 182 183 const deleted = cache.store.delete(key); 184 if (deleted) { 185 cache.stats.deletes++; 186 } 187 return deleted; 188 } 189 190 clear(cacheName) { 191 const cache = this.caches.get(cacheName); 192 if (!cache) { 193 return false; 194 } 195 196 const size = cache.store.size; 197 cache.store.clear(); 198 199 cacheEvents.emit(CacheEvents.CACHE_CLEARED, { 200 cache: cacheName, 201 entriesCleared: size, 202 timestamp: new Date(), 203 }); 204 205 logger.info(`Cleared cache: ${cacheName} (${size} entries)`); 206 return true; 207 } 208 209 clearAll() { 210 let totalCleared = 0; 211 for (const [, cache] of this.caches) { 212 totalCleared += cache.store.size; 213 cache.store.clear(); 214 } 215 216 cacheEvents.emit(CacheEvents.CACHE_CLEARED, { 217 cache: "all", 218 entriesCleared: totalCleared, 219 timestamp: new Date(), 220 }); 221 222 logger.info(`Cleared all caches (${totalCleared} entries total)`); 223 return true; 224 } 225 226 invalidateByPattern(cacheName, pattern) { 227 const cache = this.caches.get(cacheName); 228 if (!cache) { 229 return 0; 230 } 231 232 // eslint-disable-next-line security/detect-non-literal-regexp 233 const regex = new RegExp(pattern, "i"); 234 let deleted = 0; 235 236 for (const [key] of cache.store) { 237 if (regex.test(key)) { 238 cache.store.delete(key); 239 deleted++; 240 } 241 } 242 243 cacheEvents.emit(CacheEvents.CACHE_INVALIDATED, { 244 cache: cacheName, 245 pattern, 246 entriesDeleted: deleted, 247 timestamp: new Date(), 248 }); 249 250 return deleted; 251 } 252 253 getStats(cacheName) { 254 if (cacheName) { 255 const cache = this.caches.get(cacheName); 256 return cache ? { 257 size: cache.store.size, 258 maxSize: cache.maxSize, 259 ttl: cache.ttl, 260 ...cache.stats, 261 } : null; 262 } 263 264 const allStats = {}; 265 for (const [name, cache] of this.caches) { 266 allStats[name] = { 267 size: cache.store.size, 268 maxSize: cache.maxSize, 269 ttl: cache.ttl, 270 ...cache.stats, 271 }; 272 } 273 return allStats; 274 } 275 276 evictLRU(cache) { 277 let oldestKey = null; 278 let oldestTime = Date.now(); 279 280 for (const [key, item] of cache.store) { 281 if (item.lastAccessed < oldestTime) { 282 oldestTime = item.lastAccessed; 283 oldestKey = key; 284 } 285 } 286 287 if (oldestKey) { 288 cache.store.delete(oldestKey); 289 } 290 } 291 292 setupEventListeners() { 293 cacheEvents.on(CacheEvents.USER_BANNED, (data) => { 294 this.invalidateByPattern("auth", `.*:${data.userId}:.*`); 295 this.invalidateByPattern("user", `user:${data.userId}:*`); 296 this.invalidateByPattern("balance", `balance:${data.userId}:*`); 297 this.invalidateByPattern("transaction", `transaction:${data.userId}:*`); 298 this.invalidateByPattern("withdrawal", `withdrawal:${data.userId}:*`); 299 this.invalidateByPattern("deposit", `deposit:${data.userId}:*`); 300 }); 301 302 cacheEvents.on(CacheEvents.USER_UPDATED, (data) => { 303 this.invalidateByPattern("user", `user:${data.userId}:*`); 304 }); 305 306 cacheEvents.on(CacheEvents.TRANSACTION_COMPLETED, (data) => { 307 this.invalidateByPattern("transaction", `transaction:${data.userId}:*`); 308 this.invalidateByPattern("balance", `balance:${data.userId}:*`); 309 }); 310 311 cacheEvents.on(CacheEvents.WITHDRAWAL_RELEASED, (data) => { 312 this.invalidateByPattern("withdrawal", `withdrawal:${data.txId}:*`); 313 this.invalidateByPattern("balance", `balance:${data.userId}:*`); 314 }); 315 316 cacheEvents.on(CacheEvents.DEPOSIT_CREDITED, (data) => { 317 this.invalidateByPattern("balance", `balance:${data.userId}:*`); 318 this.invalidateByPattern("deposit", `deposit:${data.userId}:*`); 319 }); 320 321 cacheEvents.on(CacheEvents.NETWORK_CHANGED, () => { 322 this.clear("rpc"); 323 this.clear("block"); 324 this.clear("price"); 325 }); 326 327 cacheEvents.on(CacheEvents.BLOCK_RECEIVED, (data) => { 328 if (data.network) { 329 this.invalidateByPattern("block", `${data.network}:*`); 330 } 331 }); 332 333 cacheEvents.on(CacheEvents.RATE_UPDATED, (data) => { 334 this.clear("price"); 335 this.clear("rate"); 336 if (data.source) { 337 this.invalidateByPattern("rate", `rate:${data.source}:*`); 338 } 339 }); 340 341 cacheEvents.on(CacheEvents.SERVICE_STOPPED, (data) => { 342 if (data.service === "sweeper") { 343 this.clear("sweep"); 344 this.invalidateByPattern("sweep", "sweep:*"); 345 } else if (data.service === "depositMonitor") { 346 this.clear("deposit"); 347 this.invalidateByPattern("deposit", "deposit:*"); 348 } else if (data.service === "rateFetcher") { 349 this.clear("price"); 350 this.clear("rate"); 351 this.invalidateByPattern("rate", "rate:*"); 352 } 353 }); 354 355 cacheEvents.on(CacheEvents.PROXY_ROTATED, () => { 356 this.clear("proxy"); 357 }); 358 359 cacheEvents.on(CacheEvents.TOR_CIRCUIT_CHANGED, () => { 360 this.clear("tor"); 361 }); 362 363 cacheEvents.on(CacheEvents.SUSPICIOUS_ACTIVITY, (data) => { 364 if (data.clearCache) { 365 this.invalidateByPattern("auth", `.*:${data.userId}:.*`); 366 } 367 }); 368 369 cacheEvents.on(CacheEvents.SESSION_INVALIDATED, (data) => { 370 if (data.token) { 371 this.invalidateByPattern("auth", `token:${data.token}*`); 372 } 373 if (data.userId) { 374 this.invalidateByPattern("auth", `.*:${data.userId}:.*`); 375 this.invalidateByPattern("session", `.*:${data.userId}:.*`); 376 } 377 }); 378 379 cacheEvents.on(CacheEvents.PASSWORD_CHANGED, (data) => { 380 this.invalidateByPattern("auth", `.*:${data.userId}:.*`); 381 this.invalidateByPattern("session", `.*:${data.userId}:.*`); 382 }); 383 384 cacheEvents.on(CacheEvents.SECURITY_SETTINGS_CHANGED, (data) => { 385 this.invalidateByPattern("auth", `.*:${data.userId}:.*`); 386 this.invalidateByPattern("user", `user:${data.userId}:*`); 387 }); 388 } 389 390 startCleanup() { 391 setInterval(() => { 392 let totalCleaned = 0; 393 const now = Date.now(); 394 395 for (const [, cache] of this.caches) { 396 const toDelete = []; 397 398 for (const [key, item] of cache.store) { 399 if (now > item.expiresAt) { 400 toDelete.push(key); 401 } 402 } 403 404 toDelete.forEach(key => cache.store.delete(key)); 405 totalCleaned += toDelete.length; 406 } 407 408 if (totalCleaned > 0) { 409 logger.debug(`Cache cleanup: removed ${totalCleaned} expired entries`); 410 } 411 }, this.cleanupInterval); 412 } 413 414 warmCache(cacheName, data) { 415 const cache = this.caches.get(cacheName); 416 if (!cache) { 417 logger.warn(`Cannot warm cache: ${cacheName} not found`); 418 return; 419 } 420 421 for (const [key, value] of Object.entries(data)) { 422 this.set(cacheName, key, value); 423 } 424 425 cacheEvents.emit(CacheEvents.CACHE_WARMED, { 426 cache: cacheName, 427 entriesWarmed: Object.keys(data).length, 428 timestamp: new Date(), 429 }); 430 431 logger.info(`Warmed cache ${cacheName} with ${Object.keys(data).length} entries`); 432 } 433 434 emit = cacheEvents.emit.bind(cacheEvents); 435 on = cacheEvents.on.bind(cacheEvents); 436 off = cacheEvents.off.bind(cacheEvents); 437 removeAllListeners = cacheEvents.removeAllListeners.bind(cacheEvents); 438 } 439 440 const cacheManager = new UnifiedCacheManager(); 441 442 cacheManager.createCache("auth", { ttl: 5 * 60 * 1000, maxSize: 10000 }); 443 cacheManager.createCache("session", { ttl: 5 * 60 * 1000, maxSize: 5000 }); 444 cacheManager.createCache("user", { ttl: 10 * 60 * 1000, maxSize: 10000 }); 445 cacheManager.createCache("transaction", { ttl: 10 * 60 * 1000, maxSize: 5000 }); 446 cacheManager.createCache("withdrawal", { ttl: 5 * 60 * 1000, maxSize: 1000 }); 447 cacheManager.createCache("deposit", { ttl: 2 * 60 * 1000, maxSize: 2000 }); 448 cacheManager.createCache("balance", { ttl: 30 * 1000, maxSize: 10000 }); 449 cacheManager.createCache("rpc", { ttl: 30 * 1000, maxSize: 100 }); 450 cacheManager.createCache("marketRates", { ttl: 60 * 1000, maxSize: 500 }); 451 cacheManager.createCache("block", { ttl: 10 * 1000, maxSize: 1000 }); 452 cacheManager.createCache("proxy", { ttl: 5 * 60 * 1000, maxSize: 1000 }); 453 cacheManager.createCache("tor", { ttl: 10 * 60 * 1000, maxSize: 100 }); 454 455 export { cacheEvents }; 456 export default cacheManager;