fix: robustez de sync comprovada no emulador (duplicatas eliminadas)
- ApiClient: IllegalArgumentException de URL vira Network (banner, sem crash); DTOs com listas nullable (servidor podia enviar categories/tags: null) - SyncEngine: mutex global serializa runs (logs BEGIN/END); resolveConflict idempotente (não duplica cópia de conflito); registra rekey:<local>→<server> em meta para o editor aberto sobreviver à troca de id do create - ViewModel: resolve(id) em save/delete; createLocal enfileira create único - Outbox: índice único (noteId, op); DB v2 com fallbackToDestructiveMigration - proguard: regras corretas de kotlinx.serialization (Companion.serializer) - README: seção 'Testado em' com os cenários de emulador executados
This commit is contained in:
1 parent
2f63c7a2f5
commit
9e0bcc3e9b
9 files changed
+133
-44
No files matched your search
@@ -85,24 +85,36 @@ class NotasViewModel(app: Application) : AndroidViewModel(app) {
|
||||
createdAt = now, updatedAt = now, dirty = true,
|
||||
),
|
||||
)
|
||||
db.outbox().enqueue(OutboxEntity(noteId = id, op = "create", queuedAt = now))
|
||||
// exatamente um create na fila; autosaves seguintes não re-enfileiram
|
||||
if (!db.outbox().has(id, "create")) {
|
||||
db.outbox().enqueue(OutboxEntity(noteId = id, op = "create", queuedAt = now))
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
/** Nota por id, seguindo rekeys de create (id local → id do servidor). */
|
||||
private suspend fun resolve(id: String): String? {
|
||||
if (db.notes().byId(id) != null) return id
|
||||
val mapped = db.meta().get("rekey:$id") ?: return null
|
||||
return if (db.notes().byId(mapped) != null) mapped else null
|
||||
}
|
||||
|
||||
/** Salva edição local + enfileira mutação + agenda sync (debounce no editor). */
|
||||
fun saveNote(
|
||||
suspend fun saveNoteSync(
|
||||
id: String,
|
||||
title: String,
|
||||
format: String,
|
||||
categories: List<String>,
|
||||
content: String,
|
||||
) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val existing = db.notes().byId(id) ?: return@launch
|
||||
) {
|
||||
val targetId = resolve(id) ?: return
|
||||
val existing = db.notes().byId(targetId) ?: return
|
||||
val now = System.currentTimeMillis()
|
||||
val contentChanged = existing.content != content
|
||||
db.notes().upsert(
|
||||
existing.copy(
|
||||
id = targetId,
|
||||
title = title,
|
||||
format = format,
|
||||
categories = categories.joinToString(","),
|
||||
@@ -112,20 +124,34 @@ class NotasViewModel(app: Application) : AndroidViewModel(app) {
|
||||
conflictNote = "",
|
||||
),
|
||||
)
|
||||
if (contentChanged && !db.outbox().has(id, "create")) {
|
||||
db.outbox().enqueue(OutboxEntity(noteId = id, op = "update", queuedAt = now))
|
||||
if (contentChanged) {
|
||||
// nota em create pendente já está na fila com o conteúdo atual (upsert acima)
|
||||
if (!db.outbox().has(targetId, "create") && !db.outbox().has(targetId, "update")) {
|
||||
db.outbox().enqueue(OutboxEntity(noteId = targetId, op = "update", queuedAt = now))
|
||||
}
|
||||
SyncScheduler.runNow(getApplication(), "autosave")
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNote(
|
||||
id: String,
|
||||
title: String,
|
||||
format: String,
|
||||
categories: List<String>,
|
||||
content: String,
|
||||
) = viewModelScope.launch(Dispatchers.IO) {
|
||||
saveNoteSync(id, title, format, categories, content)
|
||||
}
|
||||
|
||||
fun deleteNote(id: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val note = db.notes().byId(id) ?: return@launch
|
||||
val targetId = resolve(id) ?: return@launch
|
||||
val note = db.notes().byId(targetId) ?: return@launch
|
||||
if (note.isLocalOnly) {
|
||||
db.notes().delete(id)
|
||||
db.outbox().dropFor(id)
|
||||
db.notes().delete(targetId)
|
||||
db.outbox().dropFor(targetId)
|
||||
} else {
|
||||
db.notes().upsert(note.copy(pendingDelete = true, dirty = true, updatedAt = System.currentTimeMillis()))
|
||||
db.outbox().enqueue(OutboxEntity(noteId = id, op = "delete", queuedAt = System.currentTimeMillis()))
|
||||
db.outbox().enqueue(OutboxEntity(noteId = targetId, op = "delete", queuedAt = System.currentTimeMillis()))
|
||||
SyncScheduler.runNow(getApplication(), "delete")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user