fix(sync): erros 5xx viram Transient com retry; SSE multilinha; trata Failed/NotConfigured

- SyncOutcome.Transient para 5xx (push/pull/serverOutcome); worker da retry
- runSync: Failed -> Error, NotConfigured respeitado (evita cast crash)
- SseClient: junta linhas data: com \n (spec SSE)
- ApiClient: remove imports sem uso
This commit is contained in:
lgcosta committed 2026-11-06 10:33:43 -03:00
1 parent e608493c81
commit d10853bcaa
4 files changed
+38 -2

No files matched your search

@@ -59,8 +59,11 @@ class SseClient(private val baseUrl: String, private val token: String, private
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
val line = reader.readLine() ?: break val line = reader.readLine() ?: break
when { when {
line.startsWith("data: ") -> data.append(line.removePrefix("data: ")) line.startsWith("data:") -> {
line.startsWith("data:") -> data.append(line.removePrefix("data:").trimStart()) // evento multilinha: junta os blocos com \n (spec SSE)
if (data.isNotEmpty()) data.append('\n')
data.append(line.removePrefix("data:").trimStart())
}
line.isEmpty() && data.isNotEmpty() -> { line.isEmpty() && data.isNotEmpty() -> {
val raw = data.toString() val raw = data.toString()
data = StringBuilder() data = StringBuilder()
@@ -22,5 +22,7 @@ sealed class SyncOutcome {
data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome() data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome()
data class NetworkDown(val message: String) : SyncOutcome() data class NetworkDown(val message: String) : SyncOutcome()
data object Unauthorized : SyncOutcome() data object Unauthorized : SyncOutcome()
/** Erro transitório do servidor (5xx): pode repetir depois. */
data class Transient(val message: String) : SyncOutcome()
data class Failed(val message: String) : SyncOutcome() data class Failed(val message: String) : SyncOutcome()
} }
@@ -81,6 +81,19 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = push.message) } status.set { it.copy(phase = SyncPhase.Offline, message = push.message) }
return push return push
} }
if (push is SyncOutcome.Transient) {
status.set { it.copy(phase = SyncPhase.Offline, message = push.message) }
return push
}
if (push is SyncOutcome.Failed) {
status.set { it.copy(phase = SyncPhase.Error, message = push.message) }
return push
}
if (push is SyncOutcome.NotConfigured) {
status.set { it.copy(phase = SyncPhase.NotConfigured) }
return push
}
val pull = pull(client) val pull = pull(client)
if (pull is SyncOutcome.Unauthorized) { if (pull is SyncOutcome.Unauthorized) {
@@ -91,6 +104,14 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) } status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) }
return pull return pull
} }
if (pull is SyncOutcome.Transient) {
status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) }
return pull
}
if (pull is SyncOutcome.Failed) {
status.set { it.copy(phase = SyncPhase.Error, message = pull.message) }
return pull
}
val ok = (push as SyncOutcome.Success) to (pull as SyncOutcome.Success) val ok = (push as SyncOutcome.Success) to (pull as SyncOutcome.Success)
val conflicts = ok.first.conflicts + ok.second.conflicts val conflicts = ok.first.conflicts + ok.second.conflicts
@@ -151,6 +172,7 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("create ${note.id}", resp)
else -> return SyncOutcome.Failed("create ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("create ${note.id}: ${describe(resp)}")
} }
} }
@@ -189,6 +211,7 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("update ${note.id}", resp)
else -> return SyncOutcome.Failed("update ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("update ${note.id}: ${describe(resp)}")
} }
} }
@@ -200,6 +223,7 @@ class SyncEngine(
is ApiResult.Ok, is ApiResult.NotFound -> Unit is ApiResult.Ok, is ApiResult.NotFound -> Unit
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("delete ${note.id}", resp)
else -> return SyncOutcome.Failed("delete ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("delete ${note.id}: ${describe(resp)}")
} }
} }
@@ -290,12 +314,14 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(cats.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(cats.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("categories", cats)
else -> {} else -> {}
} }
val list = client.listNotes() val list = client.listNotes()
if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message) if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message)
if (list is ApiResult.Unauthorized) return SyncOutcome.Unauthorized if (list is ApiResult.Unauthorized) return SyncOutcome.Unauthorized
if (list is ApiResult.Server) return serverOutcome("list", list)
if (list !is ApiResult.Ok) return SyncOutcome.Failed("list: ${describe(list)}") if (list !is ApiResult.Ok) return SyncOutcome.Failed("list: ${describe(list)}")
val serverIds = list.value.map { it.id }.toSet() val serverIds = list.value.map { it.id }.toSet()
@@ -345,6 +371,10 @@ class SyncEngine(
private fun NoteDetailDto.toEntity() = toNoteEntity() private fun NoteDetailDto.toEntity() = toNoteEntity()
private fun serverOutcome(prefix: String, r: ApiResult.Server): SyncOutcome =
if (r.code >= 500) SyncOutcome.Transient("$prefix: HTTP ${r.code} ${r.message}")
else SyncOutcome.Failed("$prefix: HTTP ${r.code} ${r.message}")
private fun describe(r: ApiResult<*>): String = when (r) { private fun describe(r: ApiResult<*>): String = when (r) {
is ApiResult.Server -> "HTTP ${r.code} ${r.message}" is ApiResult.Server -> "HTTP ${r.code} ${r.message}"
is ApiResult.Network -> r.message is ApiResult.Network -> r.message
@@ -30,6 +30,7 @@ class SyncWorker(context: Context, params: WorkerParameters) :
) )
return when (val outcome = engine.fullSync(trigger = inputData.getString(TRIGGER) ?: "auto")) { return when (val outcome = engine.fullSync(trigger = inputData.getString(TRIGGER) ?: "auto")) {
is com.labexposed.notas.domain.SyncOutcome.NetworkDown -> Result.retry() is com.labexposed.notas.domain.SyncOutcome.NetworkDown -> Result.retry()
is com.labexposed.notas.domain.SyncOutcome.Transient -> Result.retry()
is com.labexposed.notas.domain.SyncOutcome.Failed -> Result.failure() is com.labexposed.notas.domain.SyncOutcome.Failed -> Result.failure()
is com.labexposed.notas.domain.SyncOutcome.Unauthorized -> Result.success() is com.labexposed.notas.domain.SyncOutcome.Unauthorized -> Result.success()
else -> Result.success() else -> Result.success()