diff --git a/app/src/main/java/com/labexposed/notas/data/remote/SseClient.kt b/app/src/main/java/com/labexposed/notas/data/remote/SseClient.kt index 804faa1..8f40e46 100644 --- a/app/src/main/java/com/labexposed/notas/data/remote/SseClient.kt +++ b/app/src/main/java/com/labexposed/notas/data/remote/SseClient.kt @@ -59,8 +59,11 @@ class SseClient(private val baseUrl: String, private val token: String, private currentCoroutineContext().ensureActive() val line = reader.readLine() ?: break when { - line.startsWith("data: ") -> data.append(line.removePrefix("data: ")) - line.startsWith("data:") -> data.append(line.removePrefix("data:").trimStart()) + line.startsWith("data:") -> { + // 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() -> { val raw = data.toString() data = StringBuilder() diff --git a/app/src/main/java/com/labexposed/notas/domain/Models.kt b/app/src/main/java/com/labexposed/notas/domain/Models.kt index 35cba72..b02be8d 100644 --- a/app/src/main/java/com/labexposed/notas/domain/Models.kt +++ b/app/src/main/java/com/labexposed/notas/domain/Models.kt @@ -22,5 +22,7 @@ sealed class SyncOutcome { data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome() data class NetworkDown(val message: String) : 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() } diff --git a/app/src/main/java/com/labexposed/notas/domain/SyncEngine.kt b/app/src/main/java/com/labexposed/notas/domain/SyncEngine.kt index 05c12f2..b75e069 100644 --- a/app/src/main/java/com/labexposed/notas/domain/SyncEngine.kt +++ b/app/src/main/java/com/labexposed/notas/domain/SyncEngine.kt @@ -81,6 +81,19 @@ class SyncEngine( status.set { it.copy(phase = SyncPhase.Offline, message = push.message) } 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) if (pull is SyncOutcome.Unauthorized) { @@ -91,6 +104,14 @@ class SyncEngine( status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) } 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 conflicts = ok.first.conflicts + ok.second.conflicts @@ -151,6 +172,7 @@ class SyncEngine( } is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized + is ApiResult.Server -> return serverOutcome("create ${note.id}", 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.Unauthorized -> return SyncOutcome.Unauthorized + is ApiResult.Server -> return serverOutcome("update ${note.id}", 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.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized + is ApiResult.Server -> return serverOutcome("delete ${note.id}", 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.Unauthorized -> return SyncOutcome.Unauthorized + is ApiResult.Server -> return serverOutcome("categories", cats) else -> {} } val list = client.listNotes() if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message) 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)}") val serverIds = list.value.map { it.id }.toSet() @@ -345,6 +371,10 @@ class SyncEngine( 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) { is ApiResult.Server -> "HTTP ${r.code} ${r.message}" is ApiResult.Network -> r.message diff --git a/app/src/main/java/com/labexposed/notas/sync/SyncWorker.kt b/app/src/main/java/com/labexposed/notas/sync/SyncWorker.kt index 8b936c0..d723523 100644 --- a/app/src/main/java/com/labexposed/notas/sync/SyncWorker.kt +++ b/app/src/main/java/com/labexposed/notas/sync/SyncWorker.kt @@ -30,6 +30,7 @@ class SyncWorker(context: Context, params: WorkerParameters) : ) 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.Transient -> Result.retry() is com.labexposed.notas.domain.SyncOutcome.Failed -> Result.failure() is com.labexposed.notas.domain.SyncOutcome.Unauthorized -> Result.success() else -> Result.success()