Sync: mirror a pulled page in one write and read the outbox once per page, not twice per change

This commit is contained in:
jaap-jan committed 2026-10-02 14:04:36 +02:00
1 parent f997e87c5f
commit 873405e591
4 files changed
+173 -13

No files matched your search

+59
View File
@@ -118,6 +118,65 @@ public sealed class ItemStore(
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes the server's version of many items at once, in one transaction, in the order given.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="SaveAsync"/> for a page of pulled changes. A pull used to write each change on its own —
/// a context, a read, a commit — so a first sync of a keychain with a few thousand items was a few
/// thousand SQLite transactions, each one a sync to disk. This is one read for the rows that exist
/// already and one commit for the lot.
/// </para>
/// <para>
/// The same blind overwrite, and the same answer when one item appears more than once: they are applied
/// in order to the same row, so the last one given is the one kept — exactly what writing them one by one
/// would have left.
/// </para>
/// </remarks>
public async Task SaveManyAsync(IReadOnlyCollection<StoredItem> items, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(items);
if (items.Count == 0)
{
return;
}
var vaultIds = items.Select(item => item.VaultId).Distinct().ToList();
var entityIds = items.Select(item => item.EntityId).Distinct().ToList();
var context = contexts.CreateDbContext();
await using var scope = context.ConfigureAwait(false);
var existing = await context.Set<CachedItemRow>()
.Where(r => vaultIds.Contains(r.VaultId) && entityIds.Contains(r.EntityId))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var rows = existing.ToDictionary(row => (row.VaultId, row.EntityType, row.EntityId));
foreach (var item in items)
{
if (!rows.TryGetValue((item.VaultId, item.EntityType, item.EntityId), out var row))
{
row = new CachedItemRow
{
VaultId = item.VaultId,
EntityType = item.EntityType,
EntityId = item.EntityId,
};
context.Add(row);
rows.Add((item.VaultId, item.EntityType, item.EntityId), row);
}
Apply(row, item);
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>Removes a tombstone whose change has been seen by everything that needed it.</summary>
/// <remarks>
/// Only ever called for a row that is already a tombstone. Collecting a live item here would make