feat: sichtbare Seite nach eingehendem Sync automatisch neu laden
EventApplier schreibt eingehende Sync-Ereignisse absichtlich direkt auf die rohe LiteDB-Collection (Ping-Pong-Vermeidung), benachrichtigt dabei aber kein ViewModel - eine per Sync empfangene neue Stunde erschien bisher erst nach manuellem Neuladen (z.B. Tab-Wechsel). Neues SyncEngine.DataChanged-Ereignis, gefeuert nach jedem Pull mit Ereignissen sowie bei einer RemoteWon-Konfliktauflösung; SyncStatusViewModel reicht es durch. MainWindowViewModel abonniert es und lädt ausschließlich die gerade sichtbare Seite über ihren eigenen, längst vorhandenen Lade-Einstieg neu - keine Navigation weg von offenen Detailansichten, keine neue Lade-Logik, und eine laufende Inline-Bearbeitung (Schülerdetail) wird nicht überschrieben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,55 @@ public sealed class SyncEngineTests
|
||||
Assert.Equal(2, temp.Queue.GetLastServerSeq());
|
||||
}
|
||||
|
||||
// ── DataChanged: Hook für den Desktop-Client, die sichtbare Seite neu zu laden (TODO 10.1.11) ─
|
||||
|
||||
[Fact]
|
||||
public async Task DataChanged_PullMitEreignissen_Feuert()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var goodStudent = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var evt = new SyncEvent
|
||||
{
|
||||
DeviceId = "other-device", DeviceType = DeviceType.Desktop,
|
||||
EntityType = nameof(Student), EntityId = goodStudent.Id.ToString(),
|
||||
Operation = "Save", Payload = SyncCrypto.EncryptObject(goodStudent, Key), SequenceNr = 1,
|
||||
};
|
||||
var handler = new FakeHttpMessageHandler(req =>
|
||||
{
|
||||
if (req.RequestUri!.AbsolutePath == "/api/sync/pull")
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{ Content = JsonContent.Create(new PullResponse { Events = [evt], ServerSequenceNr = 1 }) };
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{ Content = JsonContent.Create(new PushResponse()) };
|
||||
});
|
||||
var engine = MakeEngine(temp, handler);
|
||||
var fired = false;
|
||||
engine.DataChanged += () => fired = true;
|
||||
|
||||
await engine.SyncNowAsync();
|
||||
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DataChanged_PullOhneEreignisse_FeuertNicht()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var handler = new FakeHttpMessageHandler(req => new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = JsonContent.Create(req.RequestUri!.AbsolutePath == "/api/sync/pull"
|
||||
? new PullResponse()
|
||||
: (object)new PushResponse()),
|
||||
});
|
||||
var engine = MakeEngine(temp, handler);
|
||||
var fired = false;
|
||||
engine.DataChanged += () => fired = true;
|
||||
|
||||
await engine.SyncNowAsync();
|
||||
|
||||
Assert.False(fired);
|
||||
}
|
||||
|
||||
/// Regression: PushAsync setzte den lokalen Pull-Cursor bisher direkt aus PushResponse.
|
||||
/// ServerSequenceNr - dem GLOBALEN Zähler über alle Geräte NACH diesem Push, nicht dem Stand,
|
||||
/// den DIESES Gerät tatsächlich per Pull erhalten hat. Hatte der Server zum Push-Zeitpunkt
|
||||
@@ -239,6 +288,8 @@ public sealed class SyncEngineTests
|
||||
});
|
||||
var applier = new EventApplier(db, Key, versions: temp.Queue);
|
||||
var engine = MakeEngine(temp, handler, applier);
|
||||
var dataChanged = false;
|
||||
engine.DataChanged += () => dataChanged = true;
|
||||
|
||||
await engine.SyncNowAsync();
|
||||
|
||||
@@ -246,6 +297,9 @@ public sealed class SyncEngineTests
|
||||
Assert.Equal("RemoteWon", conflict.Resolution);
|
||||
Assert.NotNull(db.Students.FindById(entityId));
|
||||
Assert.Equal(0, temp.Queue.PendingCount());
|
||||
// DataChanged muss auch bei einem RemoteWon-Konflikt feuern - dabei wird lokal genauso
|
||||
// Daten angewendet wie bei einem regulären Pull (TODO 10.1.11).
|
||||
Assert.True(dataChanged);
|
||||
Assert.Equal(99, temp.Queue.GetKnownServerSeq(nameof(Student), entityId.ToString()));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user