/// Lightweight in-memory cache with TTL enforcement and LRU eviction. /// /// Centralizes cache handling so services can avoid duplicating map and /// timestamp bookkeeping. Entries expire after [defaultTtl] and the cache is /// trimmed to [maxEntries] using least-recently-used eviction. class CacheManager { CacheManager({ Duration defaultTtl = const Duration(minutes: 5), int maxEntries = 64, }) : _defaultTtl = defaultTtl, _maxEntries = maxEntries; final Duration _defaultTtl; final int _maxEntries; final Map _entries = {}; /// Reads a cached value and returns whether the lookup was a hit. ({bool hit, T? value}) lookup(String key) { final record = _getRecord(key); if (record == null) return (hit: false, value: null); return (hit: true, value: record.value as T?); } /// Stores [value] with an optional [ttl] override. void write(String key, T? value, {Duration? ttl}) { final now = DateTime.now(); _entries[key] = _CacheRecord( value: value, ttl: ttl ?? _defaultTtl, createdAt: now, lastAccessed: now, ); _enforceLimits(now); } /// Removes a single cached entry. void invalidate(String key) { _entries.remove(key); } /// Removes entries that match [predicate]. void invalidateMatching(bool Function(String key) predicate) { _entries.removeWhere((key, _) => predicate(key)); } /// Clears all cached entries. void clear() { _entries.clear(); } /// Current cache statistics for debugging and health checks. Map stats() { final now = DateTime.now(); return { 'size': _entries.length, 'maxEntries': _maxEntries, 'defaultTtlSeconds': _defaultTtl.inSeconds, 'entries': _entries.map((key, record) { final age = now.difference(record.createdAt); final idle = now.difference(record.lastAccessed); return MapEntry(key, { 'ageSeconds': age.inSeconds, 'idleSeconds': idle.inSeconds, 'ttlSeconds': record.ttl.inSeconds, }); }), }; } _CacheRecord? _getRecord(String key) { final record = _entries[key]; if (record == null) return null; final now = DateTime.now(); if (record.isExpired(now)) { _entries.remove(key); return null; } record.touch(now); return record; } void _enforceLimits(DateTime now) { _removeExpired(now); if (_entries.length <= _maxEntries) return; final oldestFirst = _entries.entries.toList() ..sort((a, b) => a.value.lastAccessed.compareTo(b.value.lastAccessed)); final overflow = oldestFirst.length - _maxEntries; for (var i = 0; i < overflow; i++) { _entries.remove(oldestFirst[i].key); } } void _removeExpired(DateTime now) { _entries.removeWhere((_, record) => record.isExpired(now)); } } class _CacheRecord { _CacheRecord({ required this.value, required this.ttl, required this.createdAt, required this.lastAccessed, }); final Object? value; final Duration ttl; final DateTime createdAt; DateTime lastAccessed; bool isExpired(DateTime now) { return now.difference(createdAt) > ttl; } void touch(DateTime now) { lastAccessed = now; } }