using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; namespace StellaOps.Scheduler.WebService.Runs; internal sealed class InMemoryRunRepository : IRunRepository { private readonly ConcurrentDictionary _runs = new(StringComparer.Ordinal); public Task InsertAsync( Run run, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(run); _runs[run.Id] = run; return Task.CompletedTask; } public Task UpdateAsync( Run run, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(run); if (!_runs.TryGetValue(run.Id, out var existing)) { return Task.FromResult(false); } if (!string.Equals(existing.TenantId, run.TenantId, StringComparison.Ordinal)) { return Task.FromResult(false); } _runs[run.Id] = run; return Task.FromResult(true); } public Task GetAsync( string tenantId, string runId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(tenantId)) { throw new ArgumentException("Tenant id must be provided.", nameof(tenantId)); } if (string.IsNullOrWhiteSpace(runId)) { throw new ArgumentException("Run id must be provided.", nameof(runId)); } if (_runs.TryGetValue(runId, out var run) && string.Equals(run.TenantId, tenantId, StringComparison.Ordinal)) { return Task.FromResult(run); } return Task.FromResult(null); } public Task> ListAsync( string tenantId, RunQueryOptions? options = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(tenantId)) { throw new ArgumentException("Tenant id must be provided.", nameof(tenantId)); } options ??= new RunQueryOptions(); IEnumerable query = _runs.Values .Where(run => string.Equals(run.TenantId, tenantId, StringComparison.Ordinal)); if (!string.IsNullOrWhiteSpace(options.ScheduleId)) { query = query.Where(run => string.Equals(run.ScheduleId, options.ScheduleId, StringComparison.Ordinal)); } if (!options.States.IsDefaultOrEmpty) { var allowed = options.States.ToImmutableHashSet(); query = query.Where(run => allowed.Contains(run.State)); } if (options.CreatedAfter is { } createdAfter) { query = query.Where(run => run.CreatedAt > createdAfter); } if (options.Cursor is { } cursor) { query = options.SortAscending ? query.Where(run => run.CreatedAt > cursor.CreatedAt || (run.CreatedAt == cursor.CreatedAt && string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) > 0)) : query.Where(run => run.CreatedAt < cursor.CreatedAt || (run.CreatedAt == cursor.CreatedAt && string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) < 0)); } query = options.SortAscending ? query.OrderBy(run => run.CreatedAt).ThenBy(run => run.Id, StringComparer.Ordinal) : query.OrderByDescending(run => run.CreatedAt).ThenByDescending(run => run.Id, StringComparer.Ordinal); var limit = options.Limit is { } specified && specified > 0 ? specified : 50; var result = query.Take(limit).ToArray(); return Task.FromResult>(result); } public Task> ListByStateAsync( RunState state, int limit = 50, CancellationToken cancellationToken = default) { if (limit <= 0) { throw new ArgumentOutOfRangeException(nameof(limit), limit, "Limit must be greater than zero."); } var result = _runs.Values .Where(run => run.State == state) .OrderBy(run => run.CreatedAt) .ThenBy(run => run.Id, StringComparer.Ordinal) .Take(limit) .ToArray(); return Task.FromResult>(result); } }