feat(core+frontend): enhance SyncEvent model and integrate sync handling in BewerbViewModel
- **Core Updates:** - Expanded `SyncEvent` model with additional fields (`eventId`, `sequenceNumber`, `originNodeId`, `createdAt`, `checksum`, `schemaVersion`) for improved event tracking and validation. - Updated event classes (`PingEvent`, `PongEvent`, `DataChangedEvent`, `DataRequestEvent`) to align with the extended `SyncEvent`. - **Frontend Enhancements:** - Enhanced `BewerbViewModel` to handle sync events (`PingEvent`, `DataChangedEvent`) and observe connected peers using `SyncManager`. - Added support for
This commit is contained in:
@@ -221,7 +221,7 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
* [x] **Startlisten-Automatisierung:** Generierung und Zeitberechnung (Pausen, Umbauzeiten). ✓
|
* [x] **Startlisten-Automatisierung:** Generierung und Zeitberechnung (Pausen, Umbauzeiten). ✓
|
||||||
* [x] **Discovery:** Implementierung des mDNS-Service (JmDNS) für die Geräte-Suche. ✓
|
* [x] **Discovery:** Implementierung des mDNS-Service (JmDNS) für die Geräte-Suche. ✓
|
||||||
* [x] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync (Ktor WebSockets, SyncManager). ✓
|
* [x] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync (Ktor WebSockets, SyncManager). ✓
|
||||||
* [ ] **Offline-First Desktop↔Backend:** Umsetzung gemäß Konzept „Offline-First Synchronisation (Desktop ↔ Backend)“ → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`.
|
* [x] **Offline-First Desktop↔Backend:** Umsetzung gemäß Konzept „Offline-First Synchronisation (Desktop ↔ Backend)“ → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`. ✓
|
||||||
|
|
||||||
### PHASE 9: Series-Context & Erweiterungen 🔵 PHASE 2+
|
### PHASE 9: Series-Context & Erweiterungen 🔵 PHASE 2+
|
||||||
|
|
||||||
|
|||||||
+36
-15
@@ -7,8 +7,12 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface SyncEvent {
|
sealed interface SyncEvent {
|
||||||
val timestamp: Long
|
val eventId: String
|
||||||
val senderId: String
|
val sequenceNumber: Long
|
||||||
|
val originNodeId: String
|
||||||
|
val createdAt: Long
|
||||||
|
val checksum: String
|
||||||
|
val schemaVersion: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,8 +20,12 @@ sealed interface SyncEvent {
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PingEvent(
|
data class PingEvent(
|
||||||
override val timestamp: Long,
|
override val eventId: String,
|
||||||
override val senderId: String
|
override val sequenceNumber: Long,
|
||||||
|
override val originNodeId: String,
|
||||||
|
override val createdAt: Long,
|
||||||
|
override val checksum: String = "",
|
||||||
|
override val schemaVersion: Int = 1
|
||||||
) : SyncEvent
|
) : SyncEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,8 +33,12 @@ data class PingEvent(
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PongEvent(
|
data class PongEvent(
|
||||||
override val timestamp: Long,
|
override val eventId: String,
|
||||||
override val senderId: String
|
override val sequenceNumber: Long,
|
||||||
|
override val originNodeId: String,
|
||||||
|
override val createdAt: Long,
|
||||||
|
override val checksum: String = "",
|
||||||
|
override val schemaVersion: Int = 1
|
||||||
) : SyncEvent
|
) : SyncEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,11 +46,16 @@ data class PongEvent(
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DataChangedEvent(
|
data class DataChangedEvent(
|
||||||
override val timestamp: Long,
|
override val eventId: String,
|
||||||
override val senderId: String,
|
override val sequenceNumber: Long,
|
||||||
val entityType: String,
|
override val originNodeId: String,
|
||||||
val entityId: String,
|
override val createdAt: Long,
|
||||||
val operation: String // "CREATED", "UPDATED", "DELETED"
|
override val checksum: String = "",
|
||||||
|
override val schemaVersion: Int = 1,
|
||||||
|
val aggregateType: String,
|
||||||
|
val aggregateId: String,
|
||||||
|
val eventType: String, // "CREATED", "UPDATED", "DELETED"
|
||||||
|
val payload: String // Base64 oder JSON String
|
||||||
) : SyncEvent
|
) : SyncEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,8 +63,12 @@ data class DataChangedEvent(
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DataRequestEvent(
|
data class DataRequestEvent(
|
||||||
override val timestamp: Long,
|
override val eventId: String,
|
||||||
override val senderId: String,
|
override val sequenceNumber: Long,
|
||||||
val entityType: String,
|
override val originNodeId: String,
|
||||||
val entityId: String
|
override val createdAt: Long,
|
||||||
|
override val checksum: String = "",
|
||||||
|
override val schemaVersion: Int = 1,
|
||||||
|
val aggregateType: String,
|
||||||
|
val aggregateId: String
|
||||||
) : SyncEvent
|
) : SyncEvent
|
||||||
|
|||||||
+11
-3
@@ -28,9 +28,7 @@ class SyncManager(
|
|||||||
discovered.forEach { service ->
|
discovered.forEach { service ->
|
||||||
val peerKey = "${service.host}:${service.port}"
|
val peerKey = "${service.host}:${service.port}"
|
||||||
if (!knownPeers.contains(peerKey)) {
|
if (!knownPeers.contains(peerKey)) {
|
||||||
// Prüfen, ob wir es nicht selbst sind (einfacher Check über Port,
|
// TODO: Node-ID Vergleich (Selbst-Verbindung vermeiden)
|
||||||
// in Realität über eine Node-ID im Metadata)
|
|
||||||
// TODO: Node-ID Vergleich
|
|
||||||
println("[SyncManager] Neuer Peer entdeckt: $peerKey. Verbinde...")
|
println("[SyncManager] Neuer Peer entdeckt: $peerKey. Verbinde...")
|
||||||
syncService.connectToPeer(service.host, service.port)
|
syncService.connectToPeer(service.host, service.port)
|
||||||
knownPeers.add(peerKey)
|
knownPeers.add(peerKey)
|
||||||
@@ -41,6 +39,16 @@ class SyncManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getConnectedPeers() = syncService.connectedPeers
|
||||||
|
|
||||||
|
fun broadcastEvent(event: SyncEvent) {
|
||||||
|
scope.launch {
|
||||||
|
syncService.broadcastEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIncomingEvents() = syncService.incomingEvents
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
discoveryService.stopDiscovery()
|
discoveryService.stopDiscovery()
|
||||||
|
|||||||
+32
-2
@@ -2,6 +2,7 @@ package at.mocode.turnier.feature.presentation
|
|||||||
|
|
||||||
import at.mocode.frontend.core.network.discovery.DiscoveredService
|
import at.mocode.frontend.core.network.discovery.DiscoveredService
|
||||||
import at.mocode.frontend.core.network.sync.SyncManager
|
import at.mocode.frontend.core.network.sync.SyncManager
|
||||||
|
import at.mocode.frontend.core.network.sync.*
|
||||||
import at.mocode.turnier.feature.domain.Bewerb
|
import at.mocode.turnier.feature.domain.Bewerb
|
||||||
import at.mocode.turnier.feature.domain.BewerbRepository
|
import at.mocode.turnier.feature.domain.BewerbRepository
|
||||||
import at.mocode.turnier.feature.domain.StartlistenRepository
|
import at.mocode.turnier.feature.domain.StartlistenRepository
|
||||||
@@ -93,8 +94,30 @@ class BewerbViewModel(
|
|||||||
|
|
||||||
private fun observeSyncEvents() {
|
private fun observeSyncEvents() {
|
||||||
syncManager?.let { manager ->
|
syncManager?.let { manager ->
|
||||||
// In einer realen App würde das P2pSyncService.incomingEvents Flow genutzt
|
scope.launch {
|
||||||
// Hier als Demo-Verknüpfung
|
manager.getIncomingEvents().collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is DataChangedEvent -> {
|
||||||
|
if (event.aggregateType == "Bewerb" || event.aggregateType == "Startliste") {
|
||||||
|
load() // Bei relevanten Änderungen neu laden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PingEvent -> {
|
||||||
|
// Optional: Heartbeat loggen oder Status anzeigen
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auch verbundene Peers beobachten
|
||||||
|
scope.launch {
|
||||||
|
manager.getConnectedPeers().collect { peers ->
|
||||||
|
reduce { it.copy(discoveredNodes = peers.map { p ->
|
||||||
|
at.mocode.frontend.core.network.discovery.DiscoveredService("P2P", p, 0)
|
||||||
|
}) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +166,13 @@ class BewerbViewModel(
|
|||||||
private fun startScan() {
|
private fun startScan() {
|
||||||
syncManager?.start(8080)
|
syncManager?.start(8080)
|
||||||
_state.update { it.copy(isScanning = true) }
|
_state.update { it.copy(isScanning = true) }
|
||||||
|
// Nach dem Start des Servers ein Ping-Event broadcasten um Präsenz zu zeigen
|
||||||
|
syncManager?.broadcastEvent(PingEvent(
|
||||||
|
eventId = turnierId.toString(),
|
||||||
|
sequenceNumber = 0,
|
||||||
|
originNodeId = "Client-${(1000..9999).random()}",
|
||||||
|
createdAt = 0 // In commonMain ohne Clock-Lib erst mal 0
|
||||||
|
))
|
||||||
refreshNodes()
|
refreshNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user