summaryrefslogtreecommitdiff
path: root/Util/MemoryCache.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Util/MemoryCache.cs')
-rw-r--r--Util/MemoryCache.cs98
1 files changed, 98 insertions, 0 deletions
diff --git a/Util/MemoryCache.cs b/Util/MemoryCache.cs
new file mode 100644
index 0000000..ba314cf
--- /dev/null
+++ b/Util/MemoryCache.cs
@@ -0,0 +1,98 @@
+using System.Collections;
+
+namespace HyperBooru.Util;
+
+public class MemoryCache<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : struct {
+ /// <summary>Maximum number of items this cache may hold (unlimited if null)</summary>
+ public int? MaxItems { get; init; }
+ /// <summary>Maximum amount of time an item may stay in cache before it is removed (unlimited if null)</summary>
+ public TimeSpan? MaxAge { get; init; }
+ /// <summary>Function that will be called to populate the cache in the event of a cache-miss</summary>
+ public Func<TKey, TValue?>? DataSource { get; init; }
+
+ private Dictionary<TKey, (DateTime createTime, TValue value)> cache = new();
+
+ public TValue this[TKey key] {
+ get => GetValue(key);
+ set {
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ }
+ }
+
+ public TValue GetValue(TKey key) {
+ bool success = cache.TryGetValue(key, out var result);
+ if(success) {
+ if(MaxAge is null)
+ return result.value;
+ if(result.createTime > DateTime.Now - MaxAge)
+ return result.value;
+ }
+
+ if(DataSource is null)
+ throw new KeyNotFoundException();
+
+ TValue? value = DataSource(key);
+
+ if(value is null)
+ throw new KeyNotFoundException();
+
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ return value;
+ }
+
+ public bool TryGetValue(TKey key, out TValue? value) {
+ try {
+ value = GetValue(key);
+ return true;
+ } catch(KeyNotFoundException) {
+ value = default;
+ return false;
+ }
+ }
+
+ public bool Remove(TKey key) => cache.Remove(key);
+
+ public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ foreach(var kv in cache) {
+ // Don't return expired cache items
+ if(expiry is not null)
+ if(kv.Value.createTime < expiry)
+ continue;
+
+ yield return new KeyValuePair<TKey, TValue>(kv.Key, kv.Value.value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ private void Prune() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ // If an expiry time for cache items was
+ // specified, remove expired cache items.
+ if(expiry is not null) {
+ foreach(var kv in cache) {
+ if(kv.Value.createTime < expiry)
+ cache.Remove(kv.Key);
+ }
+ }
+
+ // If this cache was created with a maximum size,
+ // remove elements until that size is reached.
+ if(MaxItems is null || cache.Count() < MaxItems)
+ return;
+
+ var toRemove = cache
+ .OrderBy(kv => kv.Value.createTime)
+ .Take(cache.Count() - (int) MaxItems + 1)
+ .Select(kv => kv.Key)
+ .ToArray();
+
+ foreach(var key in toRemove)
+ cache.Remove(key);
+ }
+}