using adas_core.Application.Services.Interfaces; using adas_core.Domain.Models.GroupedObservations; using MongoDB.Bson; using System.Collections.Concurrent; namespace adas_core.Application.Services.Caching { /// /// Implementación de caché en memoria con soporte de locking seguro /// mediante LockManagerService + InMemoryLockProvider. /// Compatible con la interfaz ICacheService incluyendo GetOrSet. /// /// public class CacheService(LockManagerService lockManager) : ICacheService { private readonly ConcurrentDictionary _mem = new(); // HELPERS /// /// Builds a composite key for a grouped observation field, scoped to a specific patient. /// /// The grouped field whose name is included in the key. /// The identifier of the patient the key is scoped to. /// A formatted key string in the form GroupedObs:{patientId}:{gf.Name}. /// private static string BuildGroupedKey(GroupedField gf, ObjectId patientId) => $"GroupedObs:{patientId}:{gf.Name}"; // GET OR SET (string key) /// /// Retrieves an object from the in-memory cache by key, or creates and stores it using the provided factory if absent. Uses a fast path for cache hits and a lock-based path with a double-check to prevent duplicate creation across concurrent callers. /// /// The cache key used to look up and store the object. /// The asynchronous factory function invoked to produce the object when it is not found in the cache. /// Optional time-to-live associated with the cached object. /// A token to observe for cancellation requests. /// The cached or newly created object of type . /// public async Task GetOrSetObjectAsync( string key, Func> factory, TimeSpan? ttl = null, CancellationToken cancellationToken = default) { // FAST PATH if (_mem.TryGetValue(key, out var existing)) return (T)existing; // LOCKED PATH return await lockManager.WithLockAsync( $"getorset:{key}", TimeSpan.FromSeconds(5), async () => { if (_mem.TryGetValue(key, out var again)) return (T)again; var created = await factory(); if (created != null!) _mem[key] = created; return created!; }, cancellationToken); } /// /// Asynchronously retrieves the cached string value associated with the specified key, or loads and stores it using the provided loader function if absent. /// Delegates to to handle caching, honoring the optional TTL override for the cache entry. /// /// The cache key used to identify the stored string value. /// The asynchronous function invoked to load the value when no cached entry exists for the key. /// An optional time span that overrides the default time-to-live for the cached value. /// The cached or newly loaded string value, or null when the underlying cache entry is absent or cannot be cast to a string. /// public async Task GetOrSetValueAsync( string key, Func> loader, TimeSpan? ttlOverride = null) { var result = await GetOrSetObjectAsync(key, loader, ttlOverride); return (string?)result; } // GET OR SET (GroupedField + patientId) /// /// Asynchronously retrieves a cached object associated with the given grouped field and patient, or creates and caches a new one using the supplied factory when no cached value exists. /// The factory is only invoked when the cache does not contain a value for the key, and the produced value is stored in the cache only when it is not null. /// /// The grouped field that contributes to the cache key. /// The patient identifier that contributes to the cache key. /// The asynchronous factory used to build the object when no cached value is available. /// Optional time-to-live for the cached entry. /// Token used to cancel the asynchronous operation. /// A task containing the cached or newly created object. /// public async Task GetOrSetObjectAsync( GroupedField groupedField, ObjectId patientId, Func> factory, TimeSpan? ttl = null, CancellationToken cancellationToken = default) { var key = BuildGroupedKey(groupedField, patientId); if (_mem.TryGetValue(key, out var existing)) return (T)existing; return await lockManager.WithLockAsync( $"getorset:{key}", TimeSpan.FromSeconds(5), async () => { if (_mem.TryGetValue(key, out var again)) return (T)again; var created = await factory(); if (created != null!) _mem[key] = created; return created!; }, cancellationToken); } // GET / SET /// /// Stores the specified value in the in-memory collection under the given key, overwriting any existing entry. /// /// The key that identifies where the value will be stored. /// The value to associate with the specified key. /// public void SetValue(string key, string value) => _mem[key] = value; /// /// Retrieves the string representation of the value associated with the specified key from the in-memory store. /// Returns the value converted via when the key is found, or null when the key is not present. /// /// The key used to look up the value in the underlying store. /// The string representation of the stored value if the key exists; otherwise, null. /// public string? GetValue(string key) => _mem.TryGetValue(key, out var v) ? v.ToString() : null; /// /// Asynchronously retrieves an object of type from the in-memory cache using the specified key. /// Returns the stored value cast to if the key exists, or default (null for reference types) if the key is not found. /// /// The cache key used to look up the stored object. /// Indicates whether the entry's expiration should be refreshed on access. Not currently used by this implementation. /// A containing the cached value cast to , or null if no entry exists for the given key. /// public Task GetObjectAsync(string key, bool updateExpiration = true) { return Task.FromResult( _mem.TryGetValue(key, out var v) ? (T?)v : default ); } /// /// Asynchronously stores the specified object in the in-memory cache using the given key. /// /// The cache key under which the object will be stored. /// The object to store in the cache. /// Indicates whether the cache entry's expiration should be refreshed. /// public Task SetObjectAsync(string key, T obj, bool updateExpiration = true) { _mem[key] = obj!; return Task.CompletedTask; } /// /// Retrieves an object of type associated with the specified key by delegating to an overload that supports an update flag. The parameter is accepted by this overload but is not forwarded to the underlying call. /// /// The identifier of the object to retrieve. /// An optional time-to-live override accepted by this overload but ignored when delegating to the underlying retrieval call. /// A flag indicating whether the retrieval should trigger an update on the stored object. /// A task that represents the asynchronous operation, containing the retrieved object of type or null if no object is found for the given key. /// public Task GetObjectAsync(string key, TimeSpan? ttlOverride, bool upd) => GetObjectAsync(key, upd); /// /// Asynchronously stores an object associated with the specified key, with an option to override its time-to-live. The parameter is accepted but is not forwarded to the underlying storage call, so the effective time-to-live is determined elsewhere. /// /// The identifier used to store and later retrieve the object. /// The object to store in the underlying store. /// An optional time-to-live override for the stored entry; not applied by this overload. /// A flag indicating whether the operation should update an existing entry. /// A task that represents the asynchronous set operation. /// public Task SetObjectAsync(string key, T obj, TimeSpan? ttlOverride, bool upd) => SetObjectAsync(key, obj, upd); // DELETE / CLEAN /// /// Asynchronously removes the object associated with the specified key from the in-memory store. The operation succeeds silently whether or not the key exists. /// /// The identifier of the object to delete. /// public Task DeleteObjectAsync(string key) { _mem.TryRemove(key, out _); return Task.CompletedTask; } /// /// Asynchronously deletes cache entries whose keys contain the specified pattern, where asterisk (*) characters in the pattern are treated as wildcards (stripped and matched as substrings). Returns the number of entries successfully removed. /// /// The pattern to match against cache keys. Asterisk (*) characters are removed and the remaining text is used as a substring match. /// A task that represents the asynchronous operation, containing the count of entries that were removed. /// public Task DeleteByPatternAsync(string pattern) { var p = pattern.Replace("*", ""); var keys = _mem.Keys.Where(k => k.Contains(p)).ToList(); long removed = 0; foreach (var k in keys) if (_mem.TryRemove(k, out _)) removed++; return Task.FromResult(removed); } /// /// Clears all entries from the in-memory cache, removing any previously stored data. /// /// public void CleanCache() => _mem.Clear(); } }