diff --git a/src/SIL.Harmony.Tests/JsonSyncableTests.cs b/src/SIL.Harmony.Tests/JsonSyncableTests.cs index 26c3ef2..1cd8416 100644 --- a/src/SIL.Harmony.Tests/JsonSyncableTests.cs +++ b/src/SIL.Harmony.Tests/JsonSyncableTests.cs @@ -1,6 +1,4 @@ -using System.Text.Json; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using SIL.Harmony.Changes; using SIL.Harmony.Sample; using SIL.Harmony.Sample.Changes; @@ -9,10 +7,10 @@ namespace SIL.Harmony.Tests; public class JsonSyncableTests { - private static JsonSerializerOptions SerializerOptions { get; } = new ServiceCollection() + private static IJsonSyncableFactory Factory { get; } = new ServiceCollection() .AddCrdtDataSample(":memory:") .BuildServiceProvider() - .GetRequiredService(); + .GetRequiredService(); [Fact] public async Task WritesPerClientFile() @@ -20,7 +18,7 @@ public async Task WritesPerClientFile() var dir = Directory.CreateTempSubdirectory("harmony-json-syncable-test-"); try { - var syncable = new JsonSyncable(dir, SerializerOptions, NullLogger.Instance); + var syncable = Factory.Create(dir); var clientId = Guid.NewGuid(); var commit = new Commit { @@ -55,7 +53,7 @@ public async Task DoesNotDuplicateFileLines() var dir = Directory.CreateTempSubdirectory("harmony-json-syncable-test-"); try { - var syncable = new JsonSyncable(dir, SerializerOptions, NullLogger.Instance); + var syncable = Factory.Create(dir); var commit = new Commit { ClientId = Guid.NewGuid(), diff --git a/src/SIL.Harmony.Tests/Syncable/JsonSyncableSyncBackend.cs b/src/SIL.Harmony.Tests/Syncable/JsonSyncableSyncBackend.cs index ef0e6c5..6e0e7a0 100644 --- a/src/SIL.Harmony.Tests/Syncable/JsonSyncableSyncBackend.cs +++ b/src/SIL.Harmony.Tests/Syncable/JsonSyncableSyncBackend.cs @@ -1,6 +1,4 @@ -using System.Text.Json; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using SIL.Harmony.Sample; namespace SIL.Harmony.Tests.Syncable; @@ -10,17 +8,17 @@ public class JsonSyncableSyncBackend : ISyncableTestBackend public string Name => "JsonSyncable"; public override string ToString() => Name; - private JsonSerializerOptions SerializerOptions { get; } = new ServiceCollection() + private IJsonSyncableFactory Factory { get; } = new ServiceCollection() .AddCrdtDataSample(":memory:") .BuildServiceProvider() - .GetRequiredService(); + .GetRequiredService(); public async Task CreateAsync() { var testBase = new DataModelTestBase(); await testBase.InitializeAsync(); var tempDir = Directory.CreateTempSubdirectory("harmony-json-syncable-"); - var jsonSyncable = new JsonSyncable(tempDir, SerializerOptions, NullLogger.Instance); + var jsonSyncable = Factory.Create(tempDir); return new SyncableTestContext( jsonSyncable, testBase.DataModel, diff --git a/src/SIL.Harmony/CrdtKernel.cs b/src/SIL.Harmony/CrdtKernel.cs index 7466b86..ee428ee 100644 --- a/src/SIL.Harmony/CrdtKernel.cs +++ b/src/SIL.Harmony/CrdtKernel.cs @@ -75,6 +75,8 @@ public static IServiceCollection AddCrdtDataCore(this IServiceCollection service services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); //must use factory method because DataModel constructor is internal services.AddScoped(provider => new DataModel( provider.GetRequiredService(), @@ -82,7 +84,8 @@ public static IServiceCollection AddCrdtDataCore(this IServiceCollection service provider.GetRequiredService(), provider.GetRequiredService>(), provider.GetRequiredService>(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService() )); return services; } diff --git a/src/SIL.Harmony/DataModel.cs b/src/SIL.Harmony/DataModel.cs index 84c0dd3..b8b187d 100644 --- a/src/SIL.Harmony/DataModel.cs +++ b/src/SIL.Harmony/DataModel.cs @@ -25,6 +25,7 @@ public class DataModel : ISyncable, IAsyncDisposable private readonly IOptions _crdtConfig; private readonly ILogger _logger; private readonly ModelVersionService _modelVersionService; + private readonly SyncService _syncService; //constructor must be internal because CrdtRepository is internal internal DataModel(CrdtRepositoryFactory crdtRepositoryFactory, @@ -32,7 +33,8 @@ internal DataModel(CrdtRepositoryFactory crdtRepositoryFactory, IHybridDateTimeProvider timeProvider, IOptions crdtConfig, ILogger logger, - ModelVersionService modelVersionService) + ModelVersionService modelVersionService, + SyncService syncService) { _crdtRepositoryFactory = crdtRepositoryFactory; _serializerOptions = serializerOptions; @@ -40,6 +42,7 @@ internal DataModel(CrdtRepositoryFactory crdtRepositoryFactory, _crdtConfig = crdtConfig; _logger = logger; _modelVersionService = modelVersionService; + _syncService = syncService; } @@ -447,12 +450,12 @@ public async Task> GetChanges(SyncState remoteState) public async Task SyncWith(ISyncable remoteModel) { - return await SyncHelper.SyncWith(this, remoteModel, _serializerOptions); + return await _syncService.SyncWith(this, remoteModel); } public async Task SyncMany(ISyncable[] remotes) { - await SyncHelper.SyncMany(this, remotes, _serializerOptions); + await _syncService.SyncMany(this, remotes); } public ValueTask DisposeAsync() diff --git a/src/SIL.Harmony/JsonSyncable.cs b/src/SIL.Harmony/JsonSyncable.cs index 73349b5..1653aaf 100644 --- a/src/SIL.Harmony/JsonSyncable.cs +++ b/src/SIL.Harmony/JsonSyncable.cs @@ -7,22 +7,24 @@ namespace SIL.Harmony; -public class JsonSyncable : ISyncable +internal class JsonSyncable : ISyncable { private readonly DirectoryInfo _rootDir; private readonly JsonSerializerOptions _serializerOptions; private readonly ILogger _logger; + private readonly SyncService _syncService; private static readonly ConcurrentDictionary ClientLocks = new(); public const string FilenamePrefix = "client_"; public const string FilenameExtension = ".jsonl"; - public JsonSyncable(DirectoryInfo dir, JsonSerializerOptions serializerOptions, ILogger logger) + public JsonSyncable(DirectoryInfo dir, JsonSerializerOptions serializerOptions, SyncService syncService, ILogger logger) { if (!dir.Exists) dir.Create(); _rootDir = dir; _serializerOptions = serializerOptions; _logger = logger; + _syncService = syncService; } public async Task AddRangeFromSync(IEnumerable commits) @@ -95,12 +97,12 @@ await Parallel.ForEachAsync(files, async (file, ct) => public Task SyncWith(ISyncable remoteModel) { - return SyncHelper.SyncWith(this, remoteModel, _serializerOptions); + return _syncService.SyncWith(this, remoteModel); } public Task SyncMany(ISyncable[] remotes) { - return SyncHelper.SyncMany(this, remotes, _serializerOptions); + return _syncService.SyncMany(this, remotes); } public ValueTask ShouldSync() diff --git a/src/SIL.Harmony/JsonSyncableFactory.cs b/src/SIL.Harmony/JsonSyncableFactory.cs new file mode 100644 index 0000000..19c072b --- /dev/null +++ b/src/SIL.Harmony/JsonSyncableFactory.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace SIL.Harmony; + +public interface IJsonSyncableFactory +{ + /// + /// A syncable that stores commits in as one JSON Lines file (one commit per line) per client, so the folder can be shared through Dropbox, Git or a flash drive. + /// + ISyncable Create(DirectoryInfo dir); +} + +internal class JsonSyncableFactory(JsonSerializerOptions serializerOptions, SyncService syncService, ILogger logger) : IJsonSyncableFactory +{ + public ISyncable Create(DirectoryInfo dir) => new JsonSyncable(dir, serializerOptions, syncService, logger); +} diff --git a/src/SIL.Harmony/SyncHelper.cs b/src/SIL.Harmony/SyncService.cs similarity index 74% rename from src/SIL.Harmony/SyncHelper.cs rename to src/SIL.Harmony/SyncService.cs index 7eba761..ad2d377 100644 --- a/src/SIL.Harmony/SyncHelper.cs +++ b/src/SIL.Harmony/SyncService.cs @@ -2,9 +2,9 @@ using System.Text.Json; namespace SIL.Harmony; -internal static class SyncHelper +internal class SyncService(JsonSerializerOptions serializerOptions) { - public static async Task SyncWithResourceUpload(this DataModel localModel, + public async Task SyncWithResourceUpload(DataModel localModel, ISyncable remoteModel, ResourceService resourceService, IRemoteResourceService remoteResourceService, @@ -14,15 +14,9 @@ public static async Task SyncWithResourceUpload(this Dat return await localModel.SyncWith(remoteModel); } /// - /// simple sync example, each ISyncable could be over the wire or in memory - /// prefer that remote is over the wire for the best performance, however they could both be remote + /// Syncs two ISyncables, each over the wire or in memory. Prefer the remote one to be over the wire for the best performance. /// - /// - /// - /// - internal static async Task SyncWith(ISyncable localModel, - ISyncable remoteModel, - JsonSerializerOptions serializerOptions) + public async Task SyncWith(ISyncable localModel, ISyncable remoteModel) { if (!await localModel.ShouldSync() || !await remoteModel.ShouldSync()) return new SyncResults([], [], false); var localSyncState = await localModel.GetSyncState(); @@ -32,15 +26,15 @@ internal static async Task SyncWith(ISyncable localModel, if (localModel is DataModel && remoteModel is DataModel) { //cloning just to simulate the objects going over the wire - missingFromLocal = Clone(missingFromLocal, serializerOptions); - missingFromRemote = Clone(missingFromRemote, serializerOptions); + missingFromLocal = Clone(missingFromLocal); + missingFromRemote = Clone(missingFromRemote); } var pullTask = TryAddRange(localModel, missingFromLocal); var pushTask = TryAddRange(remoteModel, missingFromRemote); ThrowIfAnyErrors([await pullTask, await pushTask]); return new SyncResults(missingFromLocal, missingFromRemote, true); } - internal static async Task SyncMany(ISyncable localModel, ISyncable[] remotes, JsonSerializerOptions serializerOptions) + public async Task SyncMany(ISyncable localModel, ISyncable[] remotes) { var localSyncState = await localModel.GetSyncState(); var remoteSyncStates = new SyncState[remotes.Length]; @@ -52,7 +46,7 @@ internal static async Task SyncMany(ISyncable localModel, ISyncable[] remotes, J if (localModel is DataModel && remote is DataModel) { //cloning just to simulate the objects going over the wire - missingFromLocal = Clone(missingFromLocal, serializerOptions); + missingFromLocal = Clone(missingFromLocal); } remoteSyncStates[i] = remoteSyncState; errors.Add(await TryAddRange(localModel, missingFromLocal)); @@ -66,7 +60,7 @@ internal static async Task SyncMany(ISyncable localModel, ISyncable[] remotes, J if (localModel is DataModel && remote is DataModel) { //cloning just to simulate the objects going over the wire - missingFromRemote = Clone(missingFromRemote, serializerOptions); + missingFromRemote = Clone(missingFromRemote); } errors.Add(await TryAddRange(remote, missingFromRemote)); } @@ -94,11 +88,11 @@ private static void ThrowIfAnyErrors(IEnumerable errors) if (thrown.Length == 1) ExceptionDispatchInfo.Throw(thrown[0]); if (thrown.Length > 1) throw new AggregateException(thrown); } - private static T Clone(this T source, JsonSerializerOptions options) + private T Clone(T source) { ArgumentNullException.ThrowIfNull(source); - var json = JsonSerializer.Serialize(source, options); - var clone = JsonSerializer.Deserialize(json, options); + var json = JsonSerializer.Serialize(source, serializerOptions); + var clone = JsonSerializer.Deserialize(json, serializerOptions); return clone ?? throw new NullReferenceException("unable to clone object type " + typeof(T)); } }