Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/SIL.Harmony.Tests/JsonSyncableTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,18 +7,18 @@ 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<JsonSerializerOptions>();
.GetRequiredService<IJsonSyncableFactory>();

[Fact]
public async Task WritesPerClientFile()
{
var dir = Directory.CreateTempSubdirectory("harmony-json-syncable-test-");
try
{
var syncable = new JsonSyncable(dir, SerializerOptions, NullLogger<JsonSyncable>.Instance);
var syncable = Factory.Create(dir);
var clientId = Guid.NewGuid();
var commit = new Commit
{
Expand Down Expand Up @@ -55,7 +53,7 @@ public async Task DoesNotDuplicateFileLines()
var dir = Directory.CreateTempSubdirectory("harmony-json-syncable-test-");
try
{
var syncable = new JsonSyncable(dir, SerializerOptions, NullLogger<JsonSyncable>.Instance);
var syncable = Factory.Create(dir);
var commit = new Commit
{
ClientId = Guid.NewGuid(),
Expand Down
8 changes: 3 additions & 5 deletions src/SIL.Harmony.Tests/Syncable/JsonSyncableSyncBackend.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<JsonSerializerOptions>();
.GetRequiredService<IJsonSyncableFactory>();

public async Task<SyncableTestContext> CreateAsync()
{
var testBase = new DataModelTestBase();
await testBase.InitializeAsync();
var tempDir = Directory.CreateTempSubdirectory("harmony-json-syncable-");
var jsonSyncable = new JsonSyncable(tempDir, SerializerOptions, NullLogger<JsonSyncable>.Instance);
var jsonSyncable = Factory.Create(tempDir);
return new SyncableTestContext(
jsonSyncable,
testBase.DataModel,
Expand Down
5 changes: 4 additions & 1 deletion src/SIL.Harmony/CrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,17 @@ public static IServiceCollection AddCrdtDataCore(this IServiceCollection service
services.AddSingleton<FastProjection>();
services.AddScoped<CrdtRepositoryFactory>();
services.AddScoped<ModelVersionService>();
services.AddSingleton<SyncService>();
services.AddSingleton<IJsonSyncableFactory, JsonSyncableFactory>();
//must use factory method because DataModel constructor is internal
services.AddScoped<DataModel>(provider => new DataModel(
provider.GetRequiredService<CrdtRepositoryFactory>(),
provider.GetRequiredService<JsonSerializerOptions>(),
provider.GetRequiredService<IHybridDateTimeProvider>(),
provider.GetRequiredService<IOptions<HarmonyConfig>>(),
provider.GetRequiredService<ILogger<DataModel>>(),
provider.GetRequiredService<ModelVersionService>()
provider.GetRequiredService<ModelVersionService>(),
provider.GetRequiredService<SyncService>()
));
return services;
}
Expand Down
9 changes: 6 additions & 3 deletions src/SIL.Harmony/DataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,24 @@ public class DataModel : ISyncable, IAsyncDisposable
private readonly IOptions<HarmonyConfig> _crdtConfig;
private readonly ILogger<DataModel> _logger;
private readonly ModelVersionService _modelVersionService;
private readonly SyncService _syncService;

//constructor must be internal because CrdtRepository is internal
internal DataModel(CrdtRepositoryFactory crdtRepositoryFactory,
JsonSerializerOptions serializerOptions,
IHybridDateTimeProvider timeProvider,
IOptions<HarmonyConfig> crdtConfig,
ILogger<DataModel> logger,
ModelVersionService modelVersionService)
ModelVersionService modelVersionService,
SyncService syncService)
{
_crdtRepositoryFactory = crdtRepositoryFactory;
_serializerOptions = serializerOptions;
_timeProvider = timeProvider;
_crdtConfig = crdtConfig;
_logger = logger;
_modelVersionService = modelVersionService;
_syncService = syncService;
}


Expand Down Expand Up @@ -447,12 +450,12 @@ public async Task<ChangesResult<Commit>> GetChanges(SyncState remoteState)

public async Task<SyncResults> 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()
Expand Down
10 changes: 6 additions & 4 deletions src/SIL.Harmony/JsonSyncable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Guid, AsyncLock> 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<Commit> commits)
Expand Down Expand Up @@ -95,12 +97,12 @@ await Parallel.ForEachAsync(files, async (file, ct) =>

public Task<SyncResults> 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<bool> ShouldSync()
Expand Down
17 changes: 17 additions & 0 deletions src/SIL.Harmony/JsonSyncableFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace SIL.Harmony;

public interface IJsonSyncableFactory
{
/// <summary>
/// A syncable that stores commits in <paramref name="dir"/> as one JSON Lines file (one commit per line) per client, so the folder can be shared through Dropbox, Git or a flash drive.
/// </summary>
ISyncable Create(DirectoryInfo dir);
}

internal class JsonSyncableFactory(JsonSerializerOptions serializerOptions, SyncService syncService, ILogger<JsonSyncable> logger) : IJsonSyncableFactory
{
public ISyncable Create(DirectoryInfo dir) => new JsonSyncable(dir, serializerOptions, syncService, logger);
}
30 changes: 12 additions & 18 deletions src/SIL.Harmony/SyncHelper.cs → src/SIL.Harmony/SyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
using System.Text.Json;
namespace SIL.Harmony;

internal static class SyncHelper
internal class SyncService(JsonSerializerOptions serializerOptions)
{
public static async Task<SyncResults> SyncWithResourceUpload<TMetadata>(this DataModel localModel,
public async Task<SyncResults> SyncWithResourceUpload<TMetadata>(DataModel localModel,
ISyncable remoteModel,
ResourceService<TMetadata> resourceService,
IRemoteResourceService<TMetadata> remoteResourceService,
Expand All @@ -14,15 +14,9 @@ public static async Task<SyncResults> SyncWithResourceUpload<TMetadata>(this Dat
return await localModel.SyncWith(remoteModel);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="localModel"></param>
/// <param name="remoteModel"></param>
/// <param name="serializerOptions"></param>
internal static async Task<SyncResults> SyncWith(ISyncable localModel,
ISyncable remoteModel,
JsonSerializerOptions serializerOptions)
public async Task<SyncResults> SyncWith(ISyncable localModel, ISyncable remoteModel)
{
if (!await localModel.ShouldSync() || !await remoteModel.ShouldSync()) return new SyncResults([], [], false);
var localSyncState = await localModel.GetSyncState();
Expand All @@ -32,15 +26,15 @@ internal static async Task<SyncResults> 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];
Expand All @@ -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));
Expand All @@ -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));
}
Expand Down Expand Up @@ -94,11 +88,11 @@ private static void ThrowIfAnyErrors(IEnumerable<Exception?> errors)
if (thrown.Length == 1) ExceptionDispatchInfo.Throw(thrown[0]);
if (thrown.Length > 1) throw new AggregateException(thrown);
}
private static T Clone<T>(this T source, JsonSerializerOptions options)
private T Clone<T>(T source)
{
ArgumentNullException.ThrowIfNull(source);
var json = JsonSerializer.Serialize(source, options);
var clone = JsonSerializer.Deserialize<T>(json, options);
var json = JsonSerializer.Serialize(source, serializerOptions);
var clone = JsonSerializer.Deserialize<T>(json, serializerOptions);
return clone ?? throw new NullReferenceException("unable to clone object type " + typeof(T));
}
}
Expand Down
Loading