Sync: after a read from the beginning, remove what the server no longer has, and flag an edit that brings a forgotten item back
ci / build and test (push) Successful in 3m38s
ci / android head (push) Successful in 3m46s
ci / desktop nightly (push) Failing after 37s
ci / api image (push) Successful in 48s

The server now forgets a deletion past its retention and refuses cursors
from before it. The client then reads everything again, but everything
again says nothing about what went. So a machine away for longer than the
retention kept those items for good, and its next edit of one put it back
on the server for everyone.

A read from the beginning now marks each item it reads, keeping the mark in
the sync state because the read can span passes. When it finishes, it
removes the items of the synced types it never marked. Two kinds are kept:
items newer than the read's bound, which this machine pushed during it, and
items with a change still waiting, which are the user's work. For those,
the push finds the server without the item and sends it as a create, as
before, but now records a RecreatedAfterCollection conflict and counts it,
so the sync message says so instead of undoing someone's deletion silently.

The bound is the highest sequence the cache held when the read began, or
anything higher it has read since. Taking only what the read returned would
have left the last item of a vault in place for ever.
This commit is contained in:
jaap-jan committed 2026-10-02 17:36:11 +02:00
1 parent 55e5de0ce4
commit 858e36279d
13 files changed
+1048 -63

No files matched your search

@@ -6451,6 +6451,8 @@ internal sealed partial class VaultViewModel(
|| pass.PushedItems > 0
|| pass.Merged > 0
|| pass.Resurrected > 0
|| pass.Recreated > 0
|| pass.Pruned > 0
|| pass.DeletesAbandoned > 0
|| pass.Parked > 0
|| pass.ResyncedFromStart);
@@ -13159,13 +13161,27 @@ internal sealed partial class VaultViewModel(
+ "the beginning. "
: string.Empty;
// Not a matter for attention — the server had already deleted these, and the machine is only catching
// up — but said, because items going from the list unannounced looks like losing them.
var pruned = report.Pruned > 0
? $" {report.Pruned} item(s) deleted elsewhere while this machine was away were removed."
: string.Empty;
if (!report.NeedsAttention)
{
return replayed + (report.Pulled == 0 && report.Pushed == 0
return replayed + (report.Pulled == 0 && report.Pushed == 0 && report.Pruned == 0
? "Already up to date."
: $"Synchronised: {report.Pulled} in, {report.Pushed} out.");
: $"Synchronised: {report.Pulled} in, {report.Pushed} out.") + pruned;
}
var notes = AttentionNotes(report);
return replayed + "Synchronised, but: " + string.Join("; ", notes) + "." + pruned;
}
/// <summary>What a pass that needs attention says, one clause per thing.</summary>
private static List<string> AttentionNotes(SyncReport report)
{
var notes = new List<string>();
// "item(s)", not "host(s)": a vault now holds keys as well, and a report that named the wrong kind
@@ -13175,6 +13191,11 @@ internal sealed partial class VaultViewModel(
notes.Add($"{report.Resurrected} item(s) deleted elsewhere were kept under a new name");
}
if (report.Recreated > 0)
{
notes.Add($"{report.Recreated} item(s) deleted elsewhere long ago were put back, with this machine's changes");
}
if (report.DeletesAbandoned > 0)
{
notes.Add($"{report.DeletesAbandoned} deletion(s) were not applied because of a newer edit");
@@ -13200,7 +13221,7 @@ internal sealed partial class VaultViewModel(
+ "before you can see anything written since");
}
return replayed + "Synchronised, but: " + string.Join("; ", notes) + ".";
return notes;
}
private async Task RunAsync(string busyMessage, Func<Task> work)