/ src / utils / cache.js
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;