using Microsoft.Extensions.Logging; using StellaOps.Provcache.Entities; using System.Collections.Concurrent; using System.Text.Json; namespace StellaOps.Provcache; /// /// In-memory implementation of the revocation ledger for testing and non-persistent scenarios. /// For production use, inject a PostgreSQL-backed implementation from StellaOps.Provcache.Postgres. /// public sealed class InMemoryRevocationLedger : IRevocationLedger { private readonly ConcurrentDictionary _entries = new(); private readonly ILogger _logger; private long _currentSeqNo; public InMemoryRevocationLedger(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public Task RecordAsync( RevocationEntry entry, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(entry); var seqNo = Interlocked.Increment(ref _currentSeqNo); var recordedEntry = entry with { SeqNo = seqNo }; _entries[seqNo] = recordedEntry; _logger.LogInformation( "Recorded revocation {RevocationId} of type {Type} for key {Key}, invalidated {Count} entries", entry.RevocationId, entry.RevocationType, entry.RevokedKey, entry.EntriesInvalidated); return Task.FromResult(recordedEntry); } /// public Task> GetEntriesSinceAsync( long sinceSeqNo, int limit = 1000, CancellationToken cancellationToken = default) { var entries = _entries.Values .Where(e => e.SeqNo > sinceSeqNo) .OrderBy(e => e.SeqNo) .Take(limit) .ToList(); return Task.FromResult>(entries); } /// public Task> GetEntriesByTypeAsync( string revocationType, DateTimeOffset? since = null, int limit = 1000, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(revocationType); var query = _entries.Values .Where(e => e.RevocationType == revocationType); if (since.HasValue) { query = query.Where(e => e.RevokedAt > since.Value); } var entries = query .OrderBy(e => e.SeqNo) .Take(limit) .ToList(); return Task.FromResult>(entries); } /// public Task GetLatestSeqNoAsync(CancellationToken cancellationToken = default) { return Task.FromResult(Interlocked.Read(ref _currentSeqNo)); } /// public Task> GetRevocationsForKeyAsync( string revokedKey, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(revokedKey); var entries = _entries.Values .Where(e => e.RevokedKey == revokedKey) .OrderBy(e => e.SeqNo) .ToList(); return Task.FromResult>(entries); } /// public Task GetStatsAsync(CancellationToken cancellationToken = default) { var allEntries = _entries.Values.ToList(); var totalEntries = allEntries.Count; var latestSeqNo = Interlocked.Read(ref _currentSeqNo); var totalInvalidated = allEntries.Sum(e => (long)e.EntriesInvalidated); var entriesByType = allEntries .GroupBy(e => e.RevocationType) .ToDictionary(g => g.Key, g => (long)g.Count()); var oldestEntry = allEntries.MinBy(e => e.SeqNo)?.RevokedAt; var newestEntry = allEntries.MaxBy(e => e.SeqNo)?.RevokedAt; return Task.FromResult(new RevocationLedgerStats { TotalEntries = totalEntries, LatestSeqNo = latestSeqNo, EntriesByType = entriesByType, TotalEntriesInvalidated = totalInvalidated, OldestEntryAt = oldestEntry, NewestEntryAt = newestEntry }); } /// /// Clears all entries (for testing). /// public void Clear() { _entries.Clear(); Interlocked.Exchange(ref _currentSeqNo, 0); } }