using System.Text.Json; namespace AIStudio.Assistants.BeyondFactsLab; public sealed partial class BeyondFactsLabStore { /// /// The file of a step which holds its work state. /// private const string WORK_STATE_FILE_NAME = "work-state.json"; /// /// Loads the work state of a step. /// /// The ID of the preparation. /// The step. /// The cancellation token. /// The work state, or an empty one while the step has none. /// This version cannot open the work state. public async Task LoadWorkStateAsync(Guid projectId, BflStep step, CancellationToken token = default) { await this.InitializeAsync(token); return await this.LoadWorkStateWithoutInitializeAsync(projectId, step, token); } /// /// Loads the work states of every step of a preparation, e.g., to tell which ones are outdated. /// /// The ID of the preparation. /// The cancellation token. /// The work states this version can open, by their steps. public async Task> LoadWorkStatesAsync(Guid projectId, CancellationToken token = default) { await this.InitializeAsync(token); var workStates = new Dictionary(); foreach (var step in BflStep.All) { try { workStates[step] = await this.LoadWorkStateWithoutInitializeAsync(projectId, step, token); } catch (InvalidDataException exception) { logger.LogWarning(exception, "Could not open the work state of the step '{Step}' of the preparation '{ProjectId}' of the BeyondFacts Lab.", step, projectId); } } return workStates; } /// /// Stores the work state of a step and counts it as a change of the preparation. /// /// The ID of the preparation. /// The step. /// The work state. /// The cancellation token. /// The preparation with its new time of change. /// The preparation does not exist. /// This version cannot open the preparation or the stored work state, so it does not write them either. public async Task SaveWorkStateAsync(Guid projectId, BflStep step, BflWorkState workState, CancellationToken token = default) { await this.InitializeAsync(token); var gate = this.GetLock(projectId); await gate.WaitAsync(token); try { var project = await this.LoadRequiredWithoutInitializeAsync(projectId, token); // A work state which a newer version stored, or which is damaged, stays as it is: await this.LoadWorkStateWithoutInitializeAsync(projectId, step, token); workState.SchemaVersion = BflWorkState.CURRENT_SCHEMA_VERSION; await WriteTextAtomicAsync(this.WorkStatePath(projectId, step), JsonSerializer.Serialize(workState, JSON_OPTIONS), token); project.ModifiedAtUtc = DateTimeOffset.UtcNow; await this.StoreProjectAsync(project, token); return project; } finally { gate.Release(); } } /// /// Reads the work state of a step without the first start of the store. /// /// The ID of the preparation. /// The step. /// The cancellation token. /// The work state, or an empty one when there is no file. /// This version cannot open the work state. private async Task LoadWorkStateWithoutInitializeAsync(Guid projectId, BflStep step, CancellationToken token) { var path = this.WorkStatePath(projectId, step); if (!File.Exists(path)) return new BflWorkState(); BflWorkState? workState; try { await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); workState = await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) { throw new InvalidDataException("This version of AI Studio cannot open the work state.", exception); } if (workState is null || workState.SchemaVersion is < 1 or > BflWorkState.CURRENT_SCHEMA_VERSION || IsNull(workState.FieldSuggestions) || workState.Confirmed is { } confirmed && (IsNull(confirmed.Sections) || IsNull(confirmed.UpstreamRevisions) || IsNull(confirmed.DetailsFingerprint))) throw new InvalidDataException("This version of AI Studio cannot open the work state."); return workState; } /// /// Gets the file of the work state of a step. /// /// The ID of the preparation. /// The step. private string WorkStatePath(Guid projectId, BflStep step) => Path.Combine(this.ProjectDirectory(projectId), STEPS_DIRECTORY_NAME, step.FolderName, WORK_STATE_FILE_NAME); }