using System.Collections.Concurrent; using System.Collections.Immutable; using Connected.Interop; namespace Connected.Caching; internal abstract class Cache : ICache { private bool _disposedValue; private readonly ConcurrentDictionary _items; private readonly Task _scavenger; private readonly CancellationTokenSource _cancel = new(); public event CacheInvalidateHandler? Invalidating; public event CacheInvalidateHandler? Invalidated; public Cache() { _scavenger = new Task(OnScaveging, _cancel.Token, TaskCreationOptions.LongRunning); _items = new ConcurrentDictionary(); _scavenger.Start(); } private ConcurrentDictionary Items => _items; private CancellationTokenSource Cancel => _cancel; private void OnScaveging() { var token = Cancel.Token; while (!token.IsCancellationRequested) { try { foreach (var i in Items) i.Value.Scave(); var empties = Items.Where(f => f.Value.Count == 0).Select(f => f.Key); foreach (var i in empties) Items.TryRemove(i, out _); token.WaitHandle.WaitOne(TimeSpan.FromMinutes(1)); } catch { } } } public virtual bool IsEmpty(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.Any(); return true; } public virtual bool Exists(string key) { return Items.ContainsKey(key); } public void CreateKey(string key) { if (Exists(key)) return; Items.TryAdd(key, new Entries()); } public IEnumerator? GetEnumerator(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.GetEnumerator(); return new List().GetEnumerator(); } public virtual ImmutableList? All(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.All(); return default; } public int Count(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.Count; return 0; } public virtual T? Get(string key, Func predicate) { if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry) return GetValue(entry); return default; } public virtual async Task Get(string key, Func predicate, Func> retrieve) { if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry) return GetValue(entry); if (retrieve is null) return default; var options = new EntryOptions(); T instance = await retrieve(options); if (EqualityComparer.Default.Equals(instance, default)) { if (!options.AllowNull) return default; } if (string.IsNullOrWhiteSpace(options.Key)) throw new SysException(this, SR.ErrCacheKeyNull); Set(key, options.Key, instance, options.Duration, options.SlidingExpiration); return instance; } public virtual async Task Get(string key, object id, Func>? retrieve) { if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry) return GetValue(entry); if (retrieve is null) return default; var options = new EntryOptions { Key = id is null ? null : id.ToString() }; T instance = await retrieve(options); if (EqualityComparer.Default.Equals(instance, default)) { if (!options.AllowNull) return default; } Set(key, options.Key, instance, options.Duration, options.SlidingExpiration); return instance; } internal void ClearCore(string key) { if (Items.TryGetValue(key, out Entries? value)) value.Clear(); } public virtual async Task Clear(string key) { if (Items.TryGetValue(key, out Entries? value)) value.Clear(); await Task.CompletedTask; } public virtual T? Get(string key, object id) { if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry) return GetValue(entry); return default; } public IEntry? Get(string key, object id) { if (Items.TryGetValue(key, out Entries? value)) return value.Get(id is null ? null : id.ToString()); return default; } public virtual T? Get(string key, Func predicate) { if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry) return GetValue(entry); return default; } public virtual T? First(string key) { if (Items.TryGetValue(key, out Entries? value) && value.First() is IEntry entry) return GetValue(entry); return default; } public virtual ImmutableList? Where(string key, Func predicate) { if (Items.TryGetValue(key, out Entries? value)) return value.Where(predicate); return default; } public void CopyTo(string key, object id, IEntry instance) { if (!Items.TryGetValue(key, out Entries? value)) { value = new Entries(); if (!Items.TryAdd(key, value)) return; } value.Set(id is null ? null : id.ToString(), instance.Instance, instance.Duration, instance.SlidingExpiration); } public virtual T? Set(string key, object id, T? instance) { return Set(key, id, instance, TimeSpan.Zero); } public virtual T? Set(string key, object id, T? instance, TimeSpan duration) { return Set(key, id, instance, duration, false); } public virtual T? Set(string key, object id, T? instance, TimeSpan duration, bool slidingExpiration) { if (!Items.TryGetValue(key, out Entries? value)) { value = new Entries(); if (!Items.TryAdd(key, value)) return default; } value.Set(id is null ? null : id.ToString(), instance, duration, slidingExpiration); return instance; } internal void RemoveCore(string key, object id) { if (id is null) return; if (Items.TryGetValue(key, out Entries? value)) value.Remove(id.ToString()); } public virtual async Task Remove(string key, object id) { await Remove(key, id, true); } private async Task Remove(string key, object id, bool removing) { if (Items.TryGetValue(key, out Entries? value)) value.Remove(id is null ? null : id.ToString()); if (removing) await OnRemove(key, id); } protected virtual async Task OnRemove(string key, object id) { await Task.CompletedTask; } public async Task Invalidate(string key, object id) { await InvalidateCore(key, id, false); } internal async Task InvalidateCore(string key, object id, bool fromNotification) { /* * we store existing instance but it is not * removed from the cache yet. This is because other * threads can access this instance while we are * retrieving a new version from the source */ var existing = Get(key, id); var args = new CacheEventArgs(id is null ? null : id.ToString(), key); /* * this two events invalidate that cache reference. * note that if no new version exists the existing one * is still available to other threads. */ try { Invalidating?.Invoke(args); } catch { } try { if (!fromNotification) await OnInvalidating(args); await OnInvalidate(args); } catch { } /* * now find out if a new version has been set for the * specified key */ var newInstance = Get(key, id); /* * if no existing reference exists there is no need for * removing anything */ if (existing is not null) { /* * we have an existing instance. we are dealing with two possible scenarios: * - newInstance is null because entity has been deleted * - newInstance is actually the same instance as the existing which means a new * version does not exist. In both cases we must remove existing reference because * at this point it is not valid anymore. * note that the third case exists: reference has been replaced. in that case there * is nothing to do because Invalidating events has already replaced reference with a * new version. */ if (newInstance is null) await Remove(key, id, false); else if (existing.Equals(newInstance) && args.Behavior == InvalidateBehavior.RemoveSameInstance) await Remove(key, id, false); } try { Invalidated?.Invoke(args); } catch { } } protected internal virtual async Task OnInvalidating(CacheEventArgs e) { await Task.CompletedTask; } protected virtual async Task OnInvalidate(CacheEventArgs e) { await Task.CompletedTask; } private void Clear() { foreach (var i in Items) i.Value.Clear(); Items.Clear(); } public virtual async Task?> Remove(string key, Func predicate) { if (Items.TryGetValue(key, out Entries? value)) { var result = value.Remove(predicate); if (result is not null && result.Any()) await OnRemove(key, result); } return default; } protected virtual async Task OnRemove(string key, ImmutableList ids) { await Task.CompletedTask; } public ImmutableList? Keys(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.Keys; return default; } public ImmutableList Keys() { return Items.Keys.ToImmutableList(); } public bool Any(string key) { if (Items.TryGetValue(key, out Entries? value)) return value.Any(); return false; } private static T? GetValue(IEntry entry) { if (entry is null || entry.Instance is null) return default; if (TypeConversion.TryConvert(entry.Instance, out T? result)) return result; return default; } protected virtual void OnDisposing(bool disposing) { if (!_disposedValue) { if (disposing) { Cancel.Cancel(); Clear(); if (_scavenger is not null) { _cancel.Cancel(); if (_scavenger.IsCompleted) _scavenger.Dispose(); } } _disposedValue = true; } } public void Dispose() { OnDisposing(true); GC.SuppressFinalize(this); } }