using System; using System.Collections.Generic; using System.Text; namespace DiscordTranslator.Bot.Logic { /// /// A Least Recently Used cache implementation with forced expiry logic added on. /// https://stackoverflow.com/a/3719378/1582837 /// https://searchstorage.techtarget.com/definition/cache-algorithm /// Any extra questions just ask Taylor Love. /// /// /// public class LruCacheWithTTL where K : notnull where V : notnull { private readonly int _capacity; private readonly TimeSpan? _maxTTL; private readonly Dictionary> _map = []; private readonly LinkedList _lru = new(); private readonly Lock _sync = new(); public int Count { get { lock (_sync) { return _lru.Count; } } } public LruCacheWithTTL(int capacity) { this._capacity = capacity; _maxTTL = null; } public LruCacheWithTTL(int capacity, TimeSpan maxTTL) { this._capacity = capacity; this._maxTTL = maxTTL; } /// /// Retrieves the value by key and then attempts to cast to type T. Returns /// default(T) if value not in cache or value is not of needed type. /// /// /// /// public bool TryGetValue(K key, out T? val) where T : V { val = default(T); lock (_sync) { if (_map.TryGetValue(key, out var node)) { if (node.Value.ForceExpireAt != null && node.Value.ForceExpireAt.Value < DateTime.UtcNow) { _lru.Remove(node); _map.Remove(key); return false; } _lru.Remove(node); _lru.AddLast(node); if (node.Value.Value is T t) { val = t; return true; } } val = default; return false; } } public bool Clear() { lock (_sync) { _map.Clear(); _lru.Clear(); } return true; } public void Set(K key, V value, TimeSpan? maxTTL = null) { TimeSpan? ttl = maxTTL ?? this._maxTTL; DateTime? expiry = ttl.HasValue ? DateTime.UtcNow.Add(ttl.Value) : null; lock (_sync) { if (_map.TryGetValue(key, out var existing)) { existing.Value = existing.Value with { Value = value, ForceExpireAt = expiry }; // Refresh LRU position _lru.Remove(existing); _lru.AddLast(existing); } else { if (_map.Count >= _capacity) RemoveFirst(); // evict LRU LRUCacheItem item = new LRUCacheItem(key, value, expiry); var node = new LinkedListNode(item); _lru.AddLast(node); _map[key] = node; } } } public void Remove(K key) { lock (_sync) { if (_map.TryGetValue(key, out var node)) { _lru.Remove(node); // Removal from LinkedList is O(1) if you have direct link to the Node. _map.Remove(key); if (node.Value is IDisposable dispVal) { dispVal.Dispose(); } } } } private void RemoveFirst() { // Evict least recently used (front) if (_lru.First is { } first) { _lru.RemoveFirst(); // Remove from map using the evicted key _map.Remove(first.Value.Key); if (first.Value is IDisposable dispVal) { dispVal.Dispose(); } } } internal sealed record LRUCacheItem(K Key, V Value, DateTime? ForceExpireAt) { public override string ToString() { if (ForceExpireAt == null) { return $"{Key} => {Value} (No expiry)"; } var timeRemaining = DateTime.UtcNow - ForceExpireAt.Value; return $"{Key} => {Value} (Expires: "+timeRemaining.ToString()+")"; } } } }