Public Access
Sync: mirror a pulled page in one write and read the outbox once per page, not twice per change
This commit is contained in:
1 parent
f997e87c5f
commit
873405e591
4 files changed
+173
-13
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user