using System.Text.Json; using ClawdDotNet.Models; namespace ClawdDotNet.Services; /// /// Thread-safe JSON persistence for job execution history. /// Uses file locking to handle concurrent access from multiple schedulers. /// public sealed class JobHistoryService { private readonly string _filePath; private readonly Lock _lock = new(); private readonly int _maxEntries; private List _entries = new(); private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; public JobHistoryService(string instancePath, int maxEntries = 500) { _filePath = Path.Combine(instancePath, "job_history.json"); _maxEntries = maxEntries; Load(); } /// /// Adds a new entry to the history (thread-safe, persists immediately). /// public void Add(JobHistoryEntry entry) { lock (_lock) { _entries.Insert(0, entry); // newest first // Trim old entries if (_entries.Count > _maxEntries) _entries = _entries.Take(_maxEntries).ToList(); Save(); } } /// /// Returns a snapshot of all entries (newest first). /// public List GetAll() { lock (_lock) return new List(_entries); } private void Load() { lock (_lock) { try { if (!File.Exists(_filePath)) { _entries = new List(); return; } using var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read); _entries = JsonSerializer.Deserialize>(stream, JsonOptions) ?? new List(); } catch { _entries = new List(); } } } private void Save() { try { var tmpPath = _filePath + ".tmp"; using (var stream = new FileStream(tmpPath, FileMode.Create, FileAccess.Write, FileShare.None)) { JsonSerializer.Serialize(stream, _entries, JsonOptions); } File.Move(tmpPath, _filePath, overwrite: true); } catch { // Silently ignore write failures — next save will retry } } }