feat(sync): implement Delta-Sync API and update clients to support offline-first workflow

Added `/ping/sync` endpoint with timestamp-based Delta-Sync functionality to efficiently support offline-first clients. Extended `PingApi` and frontend clients (`PingApiClient`, `PingApiKoinClient`) with `syncPings`. Updated repository, service, and controller logic for sync handling, including new JPA query `findByCreatedAtAfter`. Adjusted test doubles and completed unit tests for backend and frontend alignment. Documented sync approach and API usage.
This commit is contained in:
2026-01-17 12:22:16 +01:00
parent 59568a42d8
commit 351fe7a672
15 changed files with 190 additions and 62 deletions
@@ -4,6 +4,7 @@ import at.mocode.ping.api.PingApi
import at.mocode.ping.api.PingResponse
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import at.mocode.ping.api.PingEvent
import at.mocode.shared.core.AppConstants
import io.ktor.client.*
import io.ktor.client.call.*
@@ -52,4 +53,10 @@ class PingApiClient(
override suspend fun securePing(): PingResponse {
return client.get("$baseUrl/api/ping/secure").body()
}
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
return client.get("$baseUrl/api/ping/sync") {
parameter("lastSyncTimestamp", lastSyncTimestamp)
}.body()
}
}
@@ -3,6 +3,7 @@ package at.mocode.clients.pingfeature
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import at.mocode.ping.api.PingApi
import at.mocode.ping.api.PingEvent
import at.mocode.ping.api.PingResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
@@ -34,4 +35,10 @@ class PingApiKoinClient(private val client: HttpClient) : PingApi {
override suspend fun securePing(): PingResponse {
return client.get("/api/ping/secure").body()
}
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
return client.get("/api/ping/sync") {
url.parameters.append("lastSyncTimestamp", lastSyncTimestamp.toString())
}.body()
}
}
@@ -4,6 +4,7 @@ import at.mocode.ping.api.PingApi
import at.mocode.ping.api.PingResponse
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import at.mocode.ping.api.PingEvent
/**
* Test double implementation of PingApi for testing purposes.
@@ -23,6 +24,7 @@ class TestPingApiClient : PingApi {
var healthResponse: HealthResponse? = null
var publicPingResponse: PingResponse? = null
var securePingResponse: PingResponse? = null
var syncPingsResponse: List<PingEvent> = emptyList()
// Call tracking
var simplePingCalled = false
@@ -30,6 +32,7 @@ class TestPingApiClient : PingApi {
var healthCheckCalled = false
var publicPingCalled = false
var securePingCalled = false
var syncPingsCalledWith: Long? = null
var callCount = 0
override suspend fun simplePing(): PingResponse {
@@ -91,6 +94,21 @@ class TestPingApiClient : PingApi {
return handleRequest(securePingResponse)
}
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
syncPingsCalledWith = lastSyncTimestamp
callCount++
if (simulateDelay) {
kotlinx.coroutines.delay(delayMs)
}
if (shouldThrowException) {
throw Exception(exceptionMessage)
}
return syncPingsResponse
}
private suspend fun handleRequest(response: PingResponse?): PingResponse {
if (simulateDelay) {
kotlinx.coroutines.delay(delayMs)
@@ -118,11 +136,13 @@ class TestPingApiClient : PingApi {
healthResponse = null
publicPingResponse = null
securePingResponse = null
syncPingsResponse = emptyList()
simplePingCalled = false
enhancedPingCalledWith = null
healthCheckCalled = false
publicPingCalled = false
securePingCalled = false
syncPingsCalledWith = null
callCount = 0
}
}