using System.Runtime.CompilerServices; namespace AIStudio.Assistants.ModelComparison; public sealed partial class ModelComparisonStore { /// /// The folder of the stored comparisons, one file per batch. /// private const string RESULTS_DIRECTORY_NAME = "results"; /// /// Gets the folder of the stored comparisons. /// private string ResultsDirectory => Path.Combine(this.RootDirectory, RESULTS_DIRECTORY_NAME); /// /// Gets the file of a stored comparison. /// /// The ID of the batch. private string ResultPath(Guid recordId) => Path.Combine(this.ResultsDirectory, $"{recordId:D}.json"); /// /// Stores what the assistant keeps of a batch, in place of what it kept of the same batch before. /// /// The stored comparison. /// The cancellation token. /// The stored comparison has no ID. public async Task SaveResultAsync(ModelComparisonRecord record, CancellationToken token = default) { if (record.Id == Guid.Empty) throw new ArgumentException("A stored comparison needs an ID.", nameof(record)); await this.InitializeAsync(token); var gate = this.GetLock(record.Id); await gate.WaitAsync(token); try { await WriteJsonAtomicAsync(this.ResultPath(record.Id), record, token); } finally { gate.Release(); } } /// /// Reads the stored comparisons one by one, so that never more than one of them is in memory at once. /// /// /// A file this version cannot read is skipped, but stays on the disk, see ModelComparisonJson. /// /// The cancellation token. /// The stored comparisons, in no particular order. public async IAsyncEnumerable ReadResultsAsync([EnumeratorCancellation] CancellationToken token = default) { await this.InitializeAsync(token); if (!Directory.Exists(this.ResultsDirectory)) yield break; foreach (var path in Directory.EnumerateFiles(this.ResultsDirectory, "*.json")) { token.ThrowIfCancellationRequested(); if (!Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var recordId)) continue; // The ID inside the file must not lead to another file than the one it was read from: if (await this.ReadJsonAsync(path, token) is { } record) yield return record with { Id = recordId }; } } /// /// Deletes all stored comparisons, the presets aside. /// /// /// Unlike reading, deleting takes the files this version cannot read along: the user asked for all results to go. /// /// The cancellation token. public async Task DeleteAllResultsAsync(CancellationToken token = default) { await this.InitializeAsync(token); if (!Directory.Exists(this.ResultsDirectory)) return; foreach (var path in Directory.EnumerateFiles(this.ResultsDirectory, "*.json").ToList()) { token.ThrowIfCancellationRequested(); if (!Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var recordId)) continue; var gate = this.GetLock(recordId); await gate.WaitAsync(token); try { File.Delete(path); } finally { gate.Release(); this.ForgetLock(recordId); } } } }