fix: Pull-Wasserzeichen sprang über fremde Ereignisse + schärfere Push-Kollisionskontrolle

EventStore.Pull gab bisher den globalen ServerSeq-Höchststand als neuen Cursor zurück statt
den höchsten unter den tatsächlich gelieferten Ereignissen - hatte ein Gerät selbst kurz zuvor
etwas gepusht, sprang sein Pull-Cursor über noch nicht abgeholte Ereignisse anderer Geräte
hinweg und verpasste sie dauerhaft, ohne jeden Fehler.

Ersetzt außerdem die bisherige 30-Sekunden-Heuristik zur Konflikterkennung beim Push durch
exakte BasedOnServerSeq-Prüfung: jedes SyncEvent trägt die ServerSeq, auf der es aufbaut: der
Server lehnt ab, wenn der aktuelle Stand nicht mehr passt. Bei Ablehnung lädt der Client sofort
den neuen Server-Stand nach, löst den Konflikt nach der bestehenden Desktop-vs-Companion/
Timestamp-Politik auf und macht ihn immer in der Konflikt-Review-UI sichtbar, statt die
verworfene Änderung stillschweigend zu verlieren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 23:25:04 +02:00
co-authored by Claude Sonnet 5
parent 270c4d40cd
commit 5bd6967421
13 changed files with 704 additions and 22 deletions
+23
View File
@@ -14,6 +14,7 @@ public class EventQueue : IDisposable
private readonly ILiteCollection<SyncMeta> _meta;
private readonly ILiteCollection<ConflictEntry> _conflicts;
private readonly ILiteCollection<PendingAttachmentUpload> _attachmentUploads;
private readonly ILiteCollection<EntityVersion> _entityVersions;
private long _currentSeq;
public EventQueue(string path)
@@ -23,6 +24,7 @@ public class EventQueue : IDisposable
_meta = _db.GetCollection<SyncMeta>("meta");
_conflicts = _db.GetCollection<ConflictEntry>("conflicts");
_attachmentUploads = _db.GetCollection<PendingAttachmentUpload>("attachment_uploads");
_entityVersions = _db.GetCollection<EntityVersion>("entity_versions");
_attachmentUploads.EnsureIndex(x => x.StorageId, unique: true);
_queue.EnsureIndex(x => x.SequenceNr);
_currentSeq = _meta.FindById("seq")?.Value ?? 0;
@@ -67,6 +69,20 @@ public class EventQueue : IDisposable
_conflicts.Update(conflict);
}
// ── Lokale Versionsverfolgung je Entität (optimistische Nebenläufigkeitskontrolle) ──────
// Merkt sich pro Entität die zuletzt bekannte ServerSeq — Grundlage für SyncEvent.
// BasedOnServerSeq beim Push (siehe SyncEngine.PushAsync) und dafür, wie ein abgelehnter
// Push nach dem Nachladen des aktuellen Server-Stands aufgelöst wird.
public long? GetKnownServerSeq(string entityType, string entityId) =>
_entityVersions.FindById(EntityVersionKey(entityType, entityId))?.ServerSeq;
public void SetKnownServerSeq(string entityType, string entityId, long serverSeq) =>
_entityVersions.Upsert(new EntityVersion
{ Key = EntityVersionKey(entityType, entityId), ServerSeq = serverSeq });
private static string EntityVersionKey(string entityType, string entityId) => $"{entityType}:{entityId}";
// ── Anhang-Warteliste (getrennt von der JSON-Ereignis-Outbox, siehe AttachmentSyncer) ────
public void QueueAttachmentUpload(string storageId)
{
@@ -103,3 +119,10 @@ internal class PendingAttachmentUpload
public ObjectId Id { get; set; } = ObjectId.NewObjectId();
public string StorageId { get; set; } = "";
}
internal class EntityVersion
{
[BsonId]
public string Key { get; set; } = "";
public long ServerSeq { get; set; }
}