From 9383a68f17ca6bdfaaf2aa7d09de9ef812f671cf Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 03:09:54 +0100 Subject: [PATCH] Release failed member snapshot requests and validate deletion frames --- .../com/utun/chat/data/MemberRepository.kt | 11 ++++++++-- .../utun/chat/data/MemberRepositoryTest.kt | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/MemberRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/MemberRepository.kt index 65915ada..a49d3e60 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/MemberRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/MemberRepository.kt @@ -8,7 +8,9 @@ import java.nio.ByteOrder data class MemberKey(val group: String, val owner: Long) /** Полные снимки CHAT-групп. Все методы вызываются последовательно на main, события не replay-ятся. - * loaded хранится отдельно: отсутствие участника в полученном списке не требует нового запроса. */ + * loaded хранится отдельно: отсутствие участника в полученном списке не требует нового запроса. + * Pending подавляет повтор 10 секунд; отказ/исключение enqueue разрешает повтор сразу, не очищая последний снимок. + * Полный кадр применяется атомарно, отдельные update/remove меняют одного участника; stop очищает всё и увеличивает generation. */ class MemberRepository( private val enqueue: (String) -> Boolean = NativeLib::requestMemberList, private val ready: () -> Boolean = NativeLib::isReady, @@ -37,7 +39,10 @@ class MemberRepository( if (started != null && now - started < 10_000) return if (started != null) log("WARN", "Snapshot response timeout channel=$group elapsedMs=${now - started}") pending[group] = now - if (!enqueue(group)) { + val accepted = try { enqueue(group) } catch (e: Exception) { + pending.remove(group); log("ERROR", "Snapshot enqueue threw channel=$group: $e"); return + } + if (!accepted) { pending.remove(group); log("ERROR", "Snapshot enqueue failed channel=$group"); return } log("DEBUG", "Snapshot requested channel=$group loaded=${group in loaded}") @@ -61,10 +66,12 @@ class MemberRepository( val previous = _members.value val updated = previous.toMutableMap() if (type == 31) { + require(data.size == 1 + length) { "invalid channel deletion size" } updated.keys.removeAll { it.group == group }; loaded.remove(group); pending.remove(group) } else if (type == 25) { require(data.size == 1 + length + 8) { "invalid removal size" } val node = ByteBuffer.wrap(data, 1 + length, 8).order(ByteOrder.LITTLE_ENDIAN).long + require(node != 0L) { "zero removal node" } updated.remove(MemberKey(group, node)) } else { val countSize = if (type == 23) 2 else 1 diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberRepositoryTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberRepositoryTest.kt index 0fae265b..875921de 100644 --- a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberRepositoryTest.kt +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberRepositoryTest.kt @@ -88,4 +88,26 @@ class MemberRepositoryTest { repo.onEvent(23, event("123", row(1))) repo.refresh("123"); assertTrue(repo.member("123", 1)!!.phoneStatusKnown) } + + @Test fun enqueueExceptionReleasesPendingAndKeepsLastSnapshot() { + var requests = 0 + val diagnostics = mutableListOf() + val repo = MemberRepository({ if (++requests == 1) error("JNI request failed") else true }, { true }, { 0 }, + { _, message -> diagnostics.add(message) }) + repo.onEvent(12, null); repo.onEvent(23, event("123", row(1))) + val before = repo.members.value + repo.refresh("123"); repo.refresh("123") + assertEquals(2, requests); assertEquals(before, repo.members.value) + assertTrue(diagnostics.any { "enqueue threw" in it }) + } + + @Test fun malformedDeletionCannotClearAnOtherwiseValidSnapshot() { + val repo = repository(); repo.onEvent(23, event("123", row(1))) + val before = repo.members.value + val header = byteArrayOf(3) + "123".toByteArray() + repo.onEvent(31, header + byteArrayOf(0)) + assertEquals(before, repo.members.value) + repo.onEvent(31, header) + assertTrue(repo.members.value.isEmpty()) + } }