Фикс синхронизации Android ↔ iOS: retry механизм и нормализация sync-курсора
- Добавлен retry для исходящих сообщений (iOS parity): 4с интервал, 3 попытки, 80с таймаут - Нормализация sync timestamp в миллисекунды (предотвращает расхождение курсора) - resolveOutgoingRetry при получении delivery ACK (0x08) - cancelAllOutgoingRetries при дисконнекте - Обновлены release notes для 1.2.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -389,8 +389,21 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
|
||||
suspend fun updateLastSyncTimestamp(timestamp: Long) {
|
||||
val account = currentAccount ?: return
|
||||
// Desktop parity: store raw cursor value from sync/update events.
|
||||
syncTimeDao.upsert(AccountSyncTimeEntity(account = account, lastSync = timestamp))
|
||||
// iOS parity: normalize timestamp to milliseconds before storing.
|
||||
// Server may send seconds or milliseconds depending on the packet.
|
||||
val normalized = normalizeSyncTimestamp(timestamp)
|
||||
if (normalized <= 0L) return
|
||||
val existing = syncTimeDao.getLastSync(account) ?: 0L
|
||||
if (normalized <= existing) return
|
||||
syncTimeDao.upsert(AccountSyncTimeEntity(account = account, lastSync = normalized))
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS parity: normalize sync timestamp to milliseconds.
|
||||
* Values below 1_000_000_000_000 are assumed to be in seconds.
|
||||
*/
|
||||
private fun normalizeSyncTimestamp(raw: Long): Long {
|
||||
return if (raw < 1_000_000_000_000L) raw * 1000L else raw
|
||||
}
|
||||
|
||||
/** Получить поток сообщений для диалога */
|
||||
@@ -589,7 +602,8 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
// 📝 LOG: Отправка пакета
|
||||
MessageLogger.logPacketSend(messageId, toPublicKey, timestamp)
|
||||
|
||||
ProtocolManager.send(packet)
|
||||
// iOS parity: send with automatic retry (4s interval, 3 attempts, 80s timeout)
|
||||
ProtocolManager.sendMessageWithRetry(packet)
|
||||
|
||||
// 📝 LOG: Успешная отправка
|
||||
MessageLogger.logSendSuccess(messageId, System.currentTimeMillis() - startTime)
|
||||
@@ -1135,7 +1149,8 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
this.attachments = emptyList() // Attachments already uploaded, tags are in content
|
||||
}
|
||||
|
||||
ProtocolManager.send(packet)
|
||||
// iOS parity: use retry mechanism for reconnect-resent messages too
|
||||
ProtocolManager.sendMessageWithRetry(packet)
|
||||
android.util.Log.d("MessageRepository", "🔄 Resent WAITING message: ${entity.messageId.take(8)}")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageRepository", "❌ Failed to retry message ${entity.messageId.take(8)}: ${e.message}")
|
||||
@@ -1158,7 +1173,7 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
* Получить ключ диалога для группировки сообщений 📁 SAVED MESSAGES: Для saved messages
|
||||
* (account == opponentKey) возвращает просто account
|
||||
*/
|
||||
private fun getDialogKey(opponentKey: String): String {
|
||||
fun getDialogKey(opponentKey: String): String {
|
||||
val account = currentAccount ?: return opponentKey
|
||||
if (isGroupDialogKey(opponentKey)) {
|
||||
return opponentKey.trim()
|
||||
@@ -1238,6 +1253,16 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API for ProtocolManager to update delivery status (e.g., marking as ERROR on retry timeout).
|
||||
*/
|
||||
suspend fun updateMessageDeliveryStatus(dialogKey: String, messageId: String, status: DeliveryStatus) {
|
||||
val account = currentAccount ?: return
|
||||
messageDao.updateDeliveryStatus(account, messageId, status.value)
|
||||
updateMessageStatus(dialogKey, messageId, status)
|
||||
_deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, messageId, status))
|
||||
}
|
||||
|
||||
private fun updateMessageStatus(dialogKey: String, messageId: String, status: DeliveryStatus) {
|
||||
messageCache[dialogKey]?.let { flow ->
|
||||
flow.value =
|
||||
|
||||
Reference in New Issue
Block a user