fix(sync): a trashed entry is no longer resurrected as a duplicate

The trash is a soft delete: no tombstone is written and the rows are absent from state.entries, so the merge read their uuids as missing locally and POSTed them back as fresh live entries. Trashing 30 entries then syncing produced 30 live duplicates alongside the trashed originals - two rows sharing one uuid.

applyRemoteSnapshot now loads the trash and skips any uuid found there. Emptying the trash is what writes the tombstone that propagates the delete.
This commit is contained in:
r-zakarya
2026-08-07 12:38:07 +01:00
parent b9b098fe12
commit c3c6bce406
2 changed files with 23 additions and 1 deletions
+14
View File
@@ -349,6 +349,17 @@ async function applyRemoteSnapshot(remote) {
const byUuid = new Map();
for (const e of state.entries) if (e.uuid) byUuid.set(e.uuid, e);
// Entries sitting in the TRASH are soft-deleted: no tombstone is written
// (that only happens on a hard delete), and they are absent from
// state.entries. Without this set the merge sees their uuids as "missing
// locally" and POSTs them back as fresh live rows — moving 30 entries to
// the trash then syncing produced 30 duplicates, live copies alongside the
// trashed originals, two rows sharing one uuid. The local decision to trash
// wins; emptying the trash writes the tombstone that propagates the delete.
await loadTrash();
const trashedUuids = new Set();
for (const e of (state.trashed || [])) if (e.uuid) trashedUuids.add(e.uuid);
// Local tombstones must veto ANY remote entry with a matching uuid —
// otherwise a perm-delete on this device gets undone by the next
// pull ("48 added" after wiping the vault, because the remote
@@ -401,6 +412,9 @@ async function applyRemoteSnapshot(remote) {
// Skip entries this device has already tombstoned — never
// resurrect a deleted entry.
if (localTombstones.has(r.uuid)) continue;
// Same for the trash: the entry exists locally, just not in the live
// list. Re-creating it would duplicate it, not restore it.
if (trashedUuids.has(r.uuid)) continue;
const local = byUuid.get(r.uuid);
if (!local) {
// New entry on remote — encrypt locally + POST keeping the uuid.