fix(sync): replicate deletions and keep photos off the wire

Reusing the backup snapshot for sync had two bugs that would bite real use:

1. Deletions didn't propagate. exportInventory() filters isDeleted=false
   (user backups are tombstone-free by design) and the JSON codec dropped
   isDeleted (omitted on encode, hardcoded false on decode) — so deleting a
   variety on one device never reached the others. Now the codec round-trips
   isDeleted (emitted only when true, so backups stay byte-identical and it's
   backward compatible), and a new exportForSync() includes tombstones. The
   importer already merges them LWW-correctly.

2. Photos bloated events. The snapshot embeds photo bytes as base64, so a
   real inventory would blow past relay event-size limits and sync would
   silently fail. exportForSync() omits attachments entirely (photos stay
   device-local; media sync is a separate, deferred concern).

buildSnapshot() now uses exportForSync(); backups keep using exportInventory
(photos in, tombstones out) unchanged.

Tests: a deletion replicates as a tombstone; the sync snapshot carries no
photo bytes while a sealed backup still does.
This commit is contained in:
vjrj 2026-07-11 00:17:08 +02:00
parent d0dbed9bc2
commit b15f912ad8
4 changed files with 83 additions and 11 deletions

View file

@ -1745,6 +1745,29 @@ class VarietyRepository {
);
}
/// Snapshots the inventory for device-to-device SYNC unlike [exportInventory]
/// this INCLUDES tombstones (so deletions replicate) and EXCLUDES attachment
/// bytes (photos are large and stay device-local for now; syncing media is a
/// separate concern). Everything else the CRDT rows with their sync metadata
/// replicates and merges LWW-by-HLC on the other device.
Future<InventorySnapshot> exportForSync() async {
final varieties = await _db.select(_db.varieties).get(); // incl. tombstones
return InventorySnapshot(
varieties: varieties,
speciesNamesById: await _scientificNamesFor(
varieties.map((v) => v.speciesId).whereType<String>().toSet(),
),
lots: await _db.select(_db.lots).get(),
vernacularNames: await _db.select(_db.varietyVernacularNames).get(),
externalLinks: await _db.select(_db.externalLinks).get(),
germinationTests: await _db.select(_db.germinationTests).get(),
conditionChecks: await _db.select(_db.conditionChecks).get(),
movements: await _db.select(_db.movements).get(),
parties: await _db.select(_db.parties).get(),
// attachments intentionally omitted photos don't ride the sync wire.
);
}
/// Imports a snapshot, reconciling by row id (UUIDv7) so nothing duplicates:
/// unknown id insert preserving the original sync metadata; known mutable
/// id last-writer-wins on the packed HLC `updatedAt`; movements are