From 61712c8e5e1e2856d3200e73c0ebad641ad2c10b Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 29 Nov 2024 16:19:56 +0700 Subject: [PATCH 01/12] Move services into own folder and namespace --- backend/FwHeadless/Program.cs | 1 + backend/FwHeadless/Services/AppVersionService.cs | 2 +- backend/FwHeadless/{ => Services}/CrdtSyncService.cs | 2 +- backend/FwHeadless/{ => Services}/ProjectLookupService.cs | 2 +- backend/FwHeadless/{ => Services}/SendReceiveHelpers.cs | 2 +- backend/FwHeadless/{ => Services}/SendReceiveService.cs | 3 +-- 6 files changed, 6 insertions(+), 6 deletions(-) rename backend/FwHeadless/{ => Services}/CrdtSyncService.cs (96%) rename backend/FwHeadless/{ => Services}/ProjectLookupService.cs (94%) rename backend/FwHeadless/{ => Services}/SendReceiveHelpers.cs (99%) rename backend/FwHeadless/{ => Services}/SendReceiveService.cs (96%) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 620635504..f0a12dc71 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -1,4 +1,5 @@ using FwHeadless; +using FwHeadless.Services; using FwDataMiniLcmBridge; using FwDataMiniLcmBridge.Api; using FwLiteProjectSync; diff --git a/backend/FwHeadless/Services/AppVersionService.cs b/backend/FwHeadless/Services/AppVersionService.cs index 4bc753899..0bc49a26a 100644 --- a/backend/FwHeadless/Services/AppVersionService.cs +++ b/backend/FwHeadless/Services/AppVersionService.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace FwHeadless; +namespace FwHeadless.Services; public static class AppVersionService { diff --git a/backend/FwHeadless/CrdtSyncService.cs b/backend/FwHeadless/Services/CrdtSyncService.cs similarity index 96% rename from backend/FwHeadless/CrdtSyncService.cs rename to backend/FwHeadless/Services/CrdtSyncService.cs index 473227e11..748d45dff 100644 --- a/backend/FwHeadless/CrdtSyncService.cs +++ b/backend/FwHeadless/Services/CrdtSyncService.cs @@ -2,7 +2,7 @@ using LcmCrdt.RemoteSync; using SIL.Harmony; -namespace FwHeadless; +namespace FwHeadless.Services; public class CrdtSyncService( CrdtHttpSyncService httpSyncService, diff --git a/backend/FwHeadless/ProjectLookupService.cs b/backend/FwHeadless/Services/ProjectLookupService.cs similarity index 94% rename from backend/FwHeadless/ProjectLookupService.cs rename to backend/FwHeadless/Services/ProjectLookupService.cs index ad3c6fa20..b29b05bc8 100644 --- a/backend/FwHeadless/ProjectLookupService.cs +++ b/backend/FwHeadless/Services/ProjectLookupService.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using SIL.Harmony.Core; -namespace FwHeadless; +namespace FwHeadless.Services; public class ProjectLookupService(LexBoxDbContext dbContext) { diff --git a/backend/FwHeadless/SendReceiveHelpers.cs b/backend/FwHeadless/Services/SendReceiveHelpers.cs similarity index 99% rename from backend/FwHeadless/SendReceiveHelpers.cs rename to backend/FwHeadless/Services/SendReceiveHelpers.cs index 68cdeddfa..a253445f3 100644 --- a/backend/FwHeadless/SendReceiveHelpers.cs +++ b/backend/FwHeadless/Services/SendReceiveHelpers.cs @@ -1,7 +1,7 @@ using FwDataMiniLcmBridge; using SIL.Progress; -namespace FwHeadless; +namespace FwHeadless.Services; public static class SendReceiveHelpers { diff --git a/backend/FwHeadless/SendReceiveService.cs b/backend/FwHeadless/Services/SendReceiveService.cs similarity index 96% rename from backend/FwHeadless/SendReceiveService.cs rename to backend/FwHeadless/Services/SendReceiveService.cs index a0508ddc3..44d00d274 100644 --- a/backend/FwHeadless/SendReceiveService.cs +++ b/backend/FwHeadless/Services/SendReceiveService.cs @@ -1,8 +1,7 @@ using FwDataMiniLcmBridge; -using FwHeadless.Services; using Microsoft.Extensions.Options; -namespace FwHeadless; +namespace FwHeadless.Services; public class SendReceiveService(IOptions config, SafeLoggingProgress progress) { From db1fdc0dda8aecfbda82593e391c6824c06401ff Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 29 Nov 2024 16:42:32 +0700 Subject: [PATCH 02/12] Add /api/crdt-sync-status API endpoint --- backend/FwHeadless/FwHeadlessKernel.cs | 1 + backend/FwHeadless/Program.cs | 13 ++++- .../Services/ProjectSyncStatusService.cs | 47 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 backend/FwHeadless/Services/ProjectSyncStatusService.cs diff --git a/backend/FwHeadless/FwHeadlessKernel.cs b/backend/FwHeadless/FwHeadlessKernel.cs index 4a7981381..f6b43bc30 100644 --- a/backend/FwHeadless/FwHeadlessKernel.cs +++ b/backend/FwHeadless/FwHeadlessKernel.cs @@ -17,6 +17,7 @@ public static void AddFwHeadless(this IServiceCollection services) .BindConfiguration("FwHeadlessConfig") .ValidateDataAnnotations() .ValidateOnStart(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index f0a12dc71..28ab150eb 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -50,6 +50,7 @@ app.MapHealthChecks("/api/healthz"); app.MapPost("/api/crdt-sync", ExecuteMergeRequest); +app.MapGet("/api/crdt-sync-status", GetMergeStatus); app.Run(); @@ -61,6 +62,7 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM FwDataFactory fwDataFactory, CrdtProjectsService projectsService, ProjectLookupService projectLookupService, + ProjectSyncStatusService syncStatusService, CrdtFwdataProjectSyncService syncService, CrdtHttpSyncService crdtHttpSyncService, IHttpClientFactory httpClientFactory, @@ -74,6 +76,8 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM return TypedResults.Ok(new SyncResult(0, 0)); } + using var syncStatusUpdater = new ProjectSyncStatusUpdater(syncStatusService, projectId); + var projectCode = await projectLookupService.GetProjectCode(projectId); if (projectCode is null) { @@ -104,7 +108,6 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM var crdtSyncService = services.GetRequiredService(); await crdtSyncService.Sync(); - var result = await syncService.Sync(miniLcmApi, fwdataApi, dryRun); logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}", result.CrdtChanges, result.FwdataChanges); @@ -114,6 +117,14 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM return TypedResults.Ok(result); } +static Results, NotFound> GetMergeStatus( + ProjectSyncStatusService syncStatusService, + Guid projectId) +{ + var status = syncStatusService.SyncStatus(projectId); + return status is null ? TypedResults.NotFound() : TypedResults.Ok(status.Value); +} + static async Task SetupFwData(FwDataProject fwDataProject, SendReceiveService srService, string projectCode, diff --git a/backend/FwHeadless/Services/ProjectSyncStatusService.cs b/backend/FwHeadless/Services/ProjectSyncStatusService.cs new file mode 100644 index 000000000..6d91aa9fc --- /dev/null +++ b/backend/FwHeadless/Services/ProjectSyncStatusService.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; + +namespace FwHeadless.Services; + +public class ProjectSyncStatusService() +{ + private ConcurrentDictionary Status { get; init; } = new(); + + public void StartSyncing(Guid projectId) + { + Status.AddOrUpdate(projectId, (_) => ProjectSyncStatus.Syncing, (_, _) => ProjectSyncStatus.Syncing); + } + + public void StopSyncing(Guid projectId) + { + Status.AddOrUpdate(projectId, (_) => ProjectSyncStatus.ReadyToSync, (_, _) => ProjectSyncStatus.ReadyToSync); + } + + public ProjectSyncStatus? SyncStatus(Guid projectId) + { + return Status.TryGetValue(projectId, out var status) ? status : null; + } +} + +public class ProjectSyncStatusUpdater : IDisposable +{ + private readonly Guid _projectId; + private readonly ProjectSyncStatusService _statusService; + + public ProjectSyncStatusUpdater(ProjectSyncStatusService statusService, Guid projectId) + { + _projectId = projectId; + _statusService = statusService; + _statusService.StartSyncing(_projectId); + } + + public void Dispose() + { + _statusService.StopSyncing(_projectId); + } +} + +public enum ProjectSyncStatus +{ + ReadyToSync, + Syncing, +} From e6a371f22851849094d3cd8fd57f24679d42e0c8 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 29 Nov 2024 16:52:53 +0700 Subject: [PATCH 03/12] Return 404 only for non-existent projects Projects that exist but have never been synced are ready to sync. --- backend/FwHeadless/Program.cs | 8 ++++++-- backend/FwHeadless/Services/ProjectLookupService.cs | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 28ab150eb..273c158b3 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -117,12 +117,16 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM return TypedResults.Ok(result); } -static Results, NotFound> GetMergeStatus( +static async Task, NotFound>> GetMergeStatus( + ProjectLookupService projectLookupService, ProjectSyncStatusService syncStatusService, Guid projectId) { var status = syncStatusService.SyncStatus(projectId); - return status is null ? TypedResults.NotFound() : TypedResults.Ok(status.Value); + if (status is not null) return TypedResults.Ok(status.Value); + // 404 only means "project doesn't exist"; if we don't know the status, then it hasn't synced before and is therefore ready to sync + if (await projectLookupService.ProjectExists(projectId)) return TypedResults.Ok(ProjectSyncStatus.ReadyToSync); + else return TypedResults.NotFound(); } static async Task SetupFwData(FwDataProject fwDataProject, diff --git a/backend/FwHeadless/Services/ProjectLookupService.cs b/backend/FwHeadless/Services/ProjectLookupService.cs index b29b05bc8..95fd9792f 100644 --- a/backend/FwHeadless/Services/ProjectLookupService.cs +++ b/backend/FwHeadless/Services/ProjectLookupService.cs @@ -15,6 +15,11 @@ public class ProjectLookupService(LexBoxDbContext dbContext) return projectCode; } + public async Task ProjectExists(Guid projectId) + { + return await dbContext.Projects.AnyAsync(p => p.Id == projectId); + } + public async Task IsCrdtProject(Guid projectId) { return await dbContext.Set().AnyAsync(c => c.ProjectId == projectId); From 937f54e5451fbba8c6ceb79ff63a9f389af9b7f7 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 2 Dec 2024 16:09:14 +0700 Subject: [PATCH 04/12] Get rid of ProjectSyncStatusUpdater class It just serves to encapsulate a Dispose() method that does a single thing, and the Defer.Action() method is perfectly adequate for that. --- backend/FwHeadless/Program.cs | 4 +++- .../Services/ProjectSyncStatusService.cs | 18 ------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 273c158b3..a9d1967a0 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using MiniLcm; using Scalar.AspNetCore; +using LexCore.Utils; var builder = WebApplication.CreateBuilder(args); @@ -76,7 +77,8 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM return TypedResults.Ok(new SyncResult(0, 0)); } - using var syncStatusUpdater = new ProjectSyncStatusUpdater(syncStatusService, projectId); + syncStatusService.StartSyncing(projectId); + using var stopSyncing = Defer.Action(() => syncStatusService.StopSyncing(projectId)); var projectCode = await projectLookupService.GetProjectCode(projectId); if (projectCode is null) diff --git a/backend/FwHeadless/Services/ProjectSyncStatusService.cs b/backend/FwHeadless/Services/ProjectSyncStatusService.cs index 6d91aa9fc..5c00fea03 100644 --- a/backend/FwHeadless/Services/ProjectSyncStatusService.cs +++ b/backend/FwHeadless/Services/ProjectSyncStatusService.cs @@ -22,24 +22,6 @@ public void StopSyncing(Guid projectId) } } -public class ProjectSyncStatusUpdater : IDisposable -{ - private readonly Guid _projectId; - private readonly ProjectSyncStatusService _statusService; - - public ProjectSyncStatusUpdater(ProjectSyncStatusService statusService, Guid projectId) - { - _projectId = projectId; - _statusService = statusService; - _statusService.StartSyncing(_projectId); - } - - public void Dispose() - { - _statusService.StopSyncing(_projectId); - } -} - public enum ProjectSyncStatus { ReadyToSync, From 641567c133f35b2e759234d38256a0795da09ff2 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 2 Dec 2024 16:09:47 +0700 Subject: [PATCH 05/12] Set up project context for all routes Next we need to figure out what to do if CurrentProjectService can't find the project. --- backend/FwHeadless/FwHeadlessKernel.cs | 1 + backend/FwHeadless/Program.cs | 7 +++++ .../Services/ProjectContextFromIdService.cs | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 backend/FwHeadless/Services/ProjectContextFromIdService.cs diff --git a/backend/FwHeadless/FwHeadlessKernel.cs b/backend/FwHeadless/FwHeadlessKernel.cs index f6b43bc30..8f4485d74 100644 --- a/backend/FwHeadless/FwHeadlessKernel.cs +++ b/backend/FwHeadless/FwHeadlessKernel.cs @@ -27,6 +27,7 @@ public static void AddFwHeadless(this IServiceCollection services) .AddFwDataBridge() .AddFwLiteProjectSync(); services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddHttpClient(LexboxHttpClientName, (provider, client) => diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index a9d1967a0..f8844f370 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -38,6 +38,13 @@ await next(); }); +// Load project ID from request +app.Use((context, next) => +{ + var renameThisService = context.RequestServices.GetRequiredService(); + return renameThisService.PopulateProjectContext(context, next); +}); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { diff --git a/backend/FwHeadless/Services/ProjectContextFromIdService.cs b/backend/FwHeadless/Services/ProjectContextFromIdService.cs new file mode 100644 index 000000000..86737ac9c --- /dev/null +++ b/backend/FwHeadless/Services/ProjectContextFromIdService.cs @@ -0,0 +1,28 @@ +using LcmCrdt; +using Microsoft.Extensions.Options; + +namespace FwHeadless.Services; + +// TODO: Pick better name +public class ProjectContextFromIdService(IOptions config, CrdtProjectsService projectsService, ProjectLookupService projectLookupService) +{ + public async Task PopulateProjectContext(HttpContext context, Func next) + { + if (context.GetProjectId() is Guid projectId) + { + var projectCode = await projectLookupService.GetProjectCode(projectId); + if (projectCode is not null) + { + var projectFolder = Path.Join(config.Value.ProjectStorageRoot, $"{projectCode}-{projectId}"); + var crdtFile = Path.Join(projectFolder, "crdt.sqlite"); + if (Path.Exists(crdtFile)) + { + var project = new CrdtProject("crdt", crdtFile); + projectsService.SetProjectScope(project); + await context.RequestServices.GetRequiredService().PopulateProjectDataCache(); + } + } + } + await next(); + } +} From 2649b68b19f0fd02dfea9b22827b77a99ec782d4 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 2 Dec 2024 17:22:25 +0700 Subject: [PATCH 06/12] Return number of commits ready to sync in status --- backend/FwHeadless/Program.cs | 22 +++++++++++++---- .../Services/ProjectSyncStatusService.cs | 24 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index f8844f370..ea694521d 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -12,6 +12,9 @@ using MiniLcm; using Scalar.AspNetCore; using LexCore.Utils; +using LinqToDB; +using SIL.Harmony.Core; +using SIL.Harmony; var builder = WebApplication.CreateBuilder(args); @@ -127,15 +130,26 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM } static async Task, NotFound>> GetMergeStatus( + ProjectContext projectContext, ProjectLookupService projectLookupService, ProjectSyncStatusService syncStatusService, + IServiceProvider services, + LexBoxDbContext lexBoxDb, Guid projectId) { var status = syncStatusService.SyncStatus(projectId); - if (status is not null) return TypedResults.Ok(status.Value); - // 404 only means "project doesn't exist"; if we don't know the status, then it hasn't synced before and is therefore ready to sync - if (await projectLookupService.ProjectExists(projectId)) return TypedResults.Ok(ProjectSyncStatus.ReadyToSync); - else return TypedResults.NotFound(); + if (status is not null) return TypedResults.Ok(new ProjectSyncStatus(status.Value, 0)); + var project = projectContext.Project; + if (project is null) + { + // 404 only means "project doesn't exist"; if we don't know the status, then it hasn't synced before and is therefore ready to sync + if (await projectLookupService.ProjectExists(projectId)) return TypedResults.Ok(ProjectSyncStatus.NeverSynced); + else return TypedResults.NotFound(); + } + var commitsOnServer = await lexBoxDb.Set().CountAsync(c => c.ProjectId == projectId); + var lcmCrdtDbContext = services.GetRequiredService(); // TODO: This *cannot* be right, can it? + var localCommits = await lcmCrdtDbContext.Set().CountAsync(); + return TypedResults.Ok(ProjectSyncStatus.ReadyToSync(localCommits - commitsOnServer)); } static async Task SetupFwData(FwDataProject fwDataProject, diff --git a/backend/FwHeadless/Services/ProjectSyncStatusService.cs b/backend/FwHeadless/Services/ProjectSyncStatusService.cs index 5c00fea03..1455c6002 100644 --- a/backend/FwHeadless/Services/ProjectSyncStatusService.cs +++ b/backend/FwHeadless/Services/ProjectSyncStatusService.cs @@ -4,26 +4,40 @@ namespace FwHeadless.Services; public class ProjectSyncStatusService() { - private ConcurrentDictionary Status { get; init; } = new(); + private ConcurrentDictionary Status { get; init; } = new(); public void StartSyncing(Guid projectId) { - Status.AddOrUpdate(projectId, (_) => ProjectSyncStatus.Syncing, (_, _) => ProjectSyncStatus.Syncing); + Status.AddOrUpdate(projectId, (_) => ProjectSyncStatusEnum.Syncing, (_, _) => ProjectSyncStatusEnum.Syncing); } public void StopSyncing(Guid projectId) { - Status.AddOrUpdate(projectId, (_) => ProjectSyncStatus.ReadyToSync, (_, _) => ProjectSyncStatus.ReadyToSync); + Status.Remove(projectId, out var _); } - public ProjectSyncStatus? SyncStatus(Guid projectId) + public ProjectSyncStatusEnum? SyncStatus(Guid projectId) { return Status.TryGetValue(projectId, out var status) ? status : null; } } -public enum ProjectSyncStatus +public enum ProjectSyncStatusEnum { + NeverSynced, ReadyToSync, Syncing, } + +// TODO: Bikeshed this name +public record ProjectSyncStatus( + ProjectSyncStatusEnum status, + int ChangesAvailable) +{ + public static ProjectSyncStatus NeverSynced => new(ProjectSyncStatusEnum.NeverSynced, 0); + public static ProjectSyncStatus Syncing => new(ProjectSyncStatusEnum.Syncing, 0); + public static ProjectSyncStatus ReadyToSync(int changes) + { + return new(ProjectSyncStatusEnum.Syncing, changes); + } +} From 71c3d3c58da7eb98189051b30a4a4a7061d74060 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 3 Dec 2024 15:45:06 +0700 Subject: [PATCH 07/12] Fix bug pointed out by Graphite AI --- backend/FwHeadless/Services/ProjectSyncStatusService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwHeadless/Services/ProjectSyncStatusService.cs b/backend/FwHeadless/Services/ProjectSyncStatusService.cs index 1455c6002..538527972 100644 --- a/backend/FwHeadless/Services/ProjectSyncStatusService.cs +++ b/backend/FwHeadless/Services/ProjectSyncStatusService.cs @@ -38,6 +38,6 @@ public record ProjectSyncStatus( public static ProjectSyncStatus Syncing => new(ProjectSyncStatusEnum.Syncing, 0); public static ProjectSyncStatus ReadyToSync(int changes) { - return new(ProjectSyncStatusEnum.Syncing, changes); + return new(ProjectSyncStatusEnum.ReadyToSync, changes); } } From 03c175b4e17b11cc9ae7b8f734fa642bb764be35 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 4 Dec 2024 11:46:33 +0700 Subject: [PATCH 08/12] Use File.Exists instead of Path.Exists Since we don't want to succeed if someone has created a directory with the name we're looking for, File.Exists is a better approach here. --- backend/FwHeadless/Services/ProjectContextFromIdService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwHeadless/Services/ProjectContextFromIdService.cs b/backend/FwHeadless/Services/ProjectContextFromIdService.cs index 86737ac9c..56c3ebc40 100644 --- a/backend/FwHeadless/Services/ProjectContextFromIdService.cs +++ b/backend/FwHeadless/Services/ProjectContextFromIdService.cs @@ -15,7 +15,7 @@ public async Task PopulateProjectContext(HttpContext context, Func next) { var projectFolder = Path.Join(config.Value.ProjectStorageRoot, $"{projectCode}-{projectId}"); var crdtFile = Path.Join(projectFolder, "crdt.sqlite"); - if (Path.Exists(crdtFile)) + if (File.Exists(crdtFile)) { var project = new CrdtProject("crdt", crdtFile); projectsService.SetProjectScope(project); From 84624ccdad133bb81d5b19ec5325d15cae3a6635 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 4 Dec 2024 13:18:50 +0700 Subject: [PATCH 09/12] Add missing file --- backend/FwHeadless/Services/HttpHelpers.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/FwHeadless/Services/HttpHelpers.cs diff --git a/backend/FwHeadless/Services/HttpHelpers.cs b/backend/FwHeadless/Services/HttpHelpers.cs new file mode 100644 index 000000000..663b8fc40 --- /dev/null +++ b/backend/FwHeadless/Services/HttpHelpers.cs @@ -0,0 +1,14 @@ +namespace FwHeadless.Services; + +public static class HttpHelpers +{ + public static Guid? GetProjectId(this HttpContext? context) + { + if (context is null) return null; + if (context.Request.Query.TryGetValue("projectId", out var projectIds) && projectIds.FirstOrDefault() is string idStr) + { + if (Guid.TryParse(idStr, out var projectId)) return projectId; + } + return null; + } +} From c934db3234046ee1b24a0255d86188e30ebac91e Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 4 Dec 2024 12:35:33 +0700 Subject: [PATCH 10/12] Add new interface implementation needed after rebase Rebasing into develop pulled in a change that LfClassicMiniLcmApi didn't yet implement. Since LF Classic doesn't handle complex forms, the implementation is dead simple: just return null. --- backend/LfClassicData/LfClassicMiniLcmApi.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index cc7945e01..0d38b75cc 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -20,6 +20,11 @@ public IAsyncEnumerable GetComplexFormTypes() return AsyncEnumerable.Empty(); } + public Task GetComplexFormType(Guid id) + { + return Task.FromResult(null); + } + public async Task GetWritingSystems() { var inputSystems = await systemDbContext.Projects.AsQueryable() From e81d56053f61fb1f793372a417acc81d9a5015cc Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 6 Dec 2024 10:04:54 +0700 Subject: [PATCH 11/12] Count commits correctly --- backend/FwHeadless/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index ea694521d..4b3d03a03 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -149,7 +149,7 @@ static async Task, NotFound>> GetMergeStatus( var commitsOnServer = await lexBoxDb.Set().CountAsync(c => c.ProjectId == projectId); var lcmCrdtDbContext = services.GetRequiredService(); // TODO: This *cannot* be right, can it? var localCommits = await lcmCrdtDbContext.Set().CountAsync(); - return TypedResults.Ok(ProjectSyncStatus.ReadyToSync(localCommits - commitsOnServer)); + return TypedResults.Ok(ProjectSyncStatus.ReadyToSync(commitsOnServer - localCommits)); } static async Task SetupFwData(FwDataProject fwDataProject, From 0ce3d2710e31357ca701489d766ecea8dd2a67c7 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 6 Dec 2024 10:09:51 +0700 Subject: [PATCH 12/12] Rename ProjectSyncStatusService so it's clearer It is now called SyncJobStatusService, to better distinguish what it does vs. what the ProjectSyncStatus represents. The job status only checks whether there's an active sync job running, while the project sync status will also check how many commits are on the server waiting to be synced. (If a sync is running right now, then the answer is 0 and we can return without hitting the database). --- backend/FwHeadless/FwHeadlessKernel.cs | 2 +- backend/FwHeadless/Program.cs | 10 +++++----- .../Services/ProjectSyncStatusService.cs | 16 +++++++++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/backend/FwHeadless/FwHeadlessKernel.cs b/backend/FwHeadless/FwHeadlessKernel.cs index 8f4485d74..3b7f64a74 100644 --- a/backend/FwHeadless/FwHeadlessKernel.cs +++ b/backend/FwHeadless/FwHeadlessKernel.cs @@ -17,7 +17,7 @@ public static void AddFwHeadless(this IServiceCollection services) .BindConfiguration("FwHeadlessConfig") .ValidateDataAnnotations() .ValidateOnStart(); - services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 4b3d03a03..7f737b755 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -73,7 +73,7 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM FwDataFactory fwDataFactory, CrdtProjectsService projectsService, ProjectLookupService projectLookupService, - ProjectSyncStatusService syncStatusService, + SyncJobStatusService syncStatusService, CrdtFwdataProjectSyncService syncService, CrdtHttpSyncService crdtHttpSyncService, IHttpClientFactory httpClientFactory, @@ -132,13 +132,13 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM static async Task, NotFound>> GetMergeStatus( ProjectContext projectContext, ProjectLookupService projectLookupService, - ProjectSyncStatusService syncStatusService, + SyncJobStatusService syncJobStatusService, IServiceProvider services, LexBoxDbContext lexBoxDb, Guid projectId) { - var status = syncStatusService.SyncStatus(projectId); - if (status is not null) return TypedResults.Ok(new ProjectSyncStatus(status.Value, 0)); + var jobStatus = syncJobStatusService.SyncStatus(projectId); + if (jobStatus == SyncJobStatus.Running) return TypedResults.Ok(ProjectSyncStatus.Syncing); var project = projectContext.Project; if (project is null) { @@ -147,7 +147,7 @@ static async Task, NotFound>> GetMergeStatus( else return TypedResults.NotFound(); } var commitsOnServer = await lexBoxDb.Set().CountAsync(c => c.ProjectId == projectId); - var lcmCrdtDbContext = services.GetRequiredService(); // TODO: This *cannot* be right, can it? + var lcmCrdtDbContext = services.GetRequiredService(); var localCommits = await lcmCrdtDbContext.Set().CountAsync(); return TypedResults.Ok(ProjectSyncStatus.ReadyToSync(commitsOnServer - localCommits)); } diff --git a/backend/FwHeadless/Services/ProjectSyncStatusService.cs b/backend/FwHeadless/Services/ProjectSyncStatusService.cs index 538527972..77d13ca85 100644 --- a/backend/FwHeadless/Services/ProjectSyncStatusService.cs +++ b/backend/FwHeadless/Services/ProjectSyncStatusService.cs @@ -2,13 +2,13 @@ namespace FwHeadless.Services; -public class ProjectSyncStatusService() +public class SyncJobStatusService() { - private ConcurrentDictionary Status { get; init; } = new(); + private ConcurrentDictionary Status { get; init; } = new(); public void StartSyncing(Guid projectId) { - Status.AddOrUpdate(projectId, (_) => ProjectSyncStatusEnum.Syncing, (_, _) => ProjectSyncStatusEnum.Syncing); + Status.AddOrUpdate(projectId, (_) => SyncJobStatus.Running, (_, _) => SyncJobStatus.Running); } public void StopSyncing(Guid projectId) @@ -16,12 +16,18 @@ public void StopSyncing(Guid projectId) Status.Remove(projectId, out var _); } - public ProjectSyncStatusEnum? SyncStatus(Guid projectId) + public SyncJobStatus SyncStatus(Guid projectId) { - return Status.TryGetValue(projectId, out var status) ? status : null; + return Status.TryGetValue(projectId, out var status) ? status : SyncJobStatus.NotRunning; } } +public enum SyncJobStatus +{ + NotRunning, + Running, +} + public enum ProjectSyncStatusEnum { NeverSynced,