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()+")";
}
}
}
}