Skip to content

Commit c99a2af

Browse files
authored
feat: mem cache convo list, discard decode failures (#80)
1 parent 6d2b752 commit c99a2af

File tree

11 files changed

+145
-55
lines changed

11 files changed

+145
-55
lines changed

library/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
id 'org.jetbrains.kotlin.android'
66
id 'com.google.protobuf' version '0.9.1'
77
id "org.jlleitschuh.gradle.ktlint" version "11.0.0"
8-
id 'org.jetbrains.dokka'
8+
id "org.jetbrains.dokka" version "1.8.10"
99
}
1010

1111
dokkaGfmPartial {

library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class ConversationTest {
7373

7474
@Test
7575
fun testCanInitiateV2Conversation() {
76-
val existingConversations = aliceClient.conversations.conversations
76+
val existingConversations = aliceClient.conversations.conversationsByTopic
7777
assert(existingConversations.isEmpty())
7878
val conversation = bobClient.conversations.newConversation(alice.walletAddress)
7979
val aliceInviteMessage =
@@ -227,22 +227,22 @@ class ConversationTest {
227227
)
228228
val tamperedMessage =
229229
MessageV2Builder.buildFromCipherText(headerBytes = headerBytes, ciphertext = ciphertext)
230-
aliceClient.publish(
231-
envelopes = listOf(
232-
EnvelopeBuilder.buildFromString(
233-
topic = aliceConversation.topic,
234-
timestamp = Date(),
235-
message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage).toByteArray()
236-
)
230+
val tamperedEnvelope =
231+
EnvelopeBuilder.buildFromString(
232+
topic = aliceConversation.topic,
233+
timestamp = Date(),
234+
message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage).toByteArray()
237235
)
238-
)
236+
aliceClient.publish(envelopes = listOf(tamperedEnvelope))
239237
val bobConversation = bobClient.conversations.newConversation(
240238
aliceWallet.address,
241239
InvitationV1ContextBuilder.buildFromConversation("hi")
242240
)
243241
assertThrows("Invalid signature", XMTPException::class.java) {
244-
val messages = bobConversation.messages()
242+
bobConversation.decode(tamperedEnvelope)
245243
}
244+
// But it should be properly discarded from the message listing.
245+
assertEquals(0, bobConversation.messages().size)
246246
}
247247

248248
@Test

library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt

+51
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.xmtp.android.library.messages.toPublicKeyBundle
2222
import org.xmtp.android.library.messages.walletAddress
2323
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest
2424
import org.xmtp.proto.message.contents.Contact
25+
import org.xmtp.proto.message.contents.InvitationV1Kt.context
2526
import org.xmtp.proto.message.contents.PrivateKeyOuterClass
2627
import java.util.Date
2728

@@ -140,6 +141,51 @@ class LocalInstrumentedTest {
140141
assertEquals("now", nowMessage2.body)
141142
}
142143

144+
@Test
145+
fun testListingConversations() {
146+
val alice = Client().create(
147+
PrivateKeyBuilder(),
148+
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
149+
)
150+
val bob = Client().create(
151+
PrivateKeyBuilder(),
152+
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
153+
)
154+
155+
// First Bob starts a conversation with Alice
156+
val c1 = bob.conversations.newConversation(
157+
alice.address,
158+
context = context {
159+
conversationId = "example.com/alice-bob-1"
160+
metadata["title"] = "First Chat"
161+
}
162+
)
163+
c1.send("hello Alice!")
164+
delayToPropagate()
165+
166+
// So Alice should see just that one conversation.
167+
var aliceConvoList = alice.conversations.list()
168+
assertEquals(1, aliceConvoList.size)
169+
assertEquals("example.com/alice-bob-1", aliceConvoList[0].conversationId)
170+
171+
// And later when Bob starts a second conversation with Alice
172+
val c2 = bob.conversations.newConversation(
173+
alice.address,
174+
context = context {
175+
conversationId = "example.com/alice-bob-2"
176+
metadata["title"] = "Second Chat"
177+
}
178+
)
179+
c2.send("hello again Alice!")
180+
delayToPropagate()
181+
182+
// Then Alice should see both conversations, the newer one first.
183+
aliceConvoList = alice.conversations.list()
184+
assertEquals(2, aliceConvoList.size)
185+
assertEquals("example.com/alice-bob-2", aliceConvoList[0].conversationId)
186+
assertEquals("example.com/alice-bob-1", aliceConvoList[1].conversationId)
187+
}
188+
143189
@Test
144190
fun testCanPaginateV1Messages() {
145191
val bob = PrivateKeyBuilder()
@@ -247,4 +293,9 @@ class LocalInstrumentedTest {
247293

248294
assertEquals(result.responsesOrBuilderList.size, 1)
249295
}
296+
297+
// A delay to allow messages to propagate before making assertions.
298+
private fun delayToPropagate() {
299+
Thread.sleep(500)
300+
}
250301
}

library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ class FakeApiClient : ApiClient {
140140
}.reversed()
141141
)
142142

143-
val startAt = pagination?.startTime
143+
val startAt = pagination?.before
144144
if (startAt != null) {
145145
result = result.filter { it.timestampNs < startAt.time * 1_000_000 }
146146
.sortedBy { it.timestampNs }.toMutableList()
147147
}
148-
val endAt = pagination?.endTime
148+
val endAt = pagination?.after
149149
if (endAt != null) {
150150
result = result.filter { it.timestampNs > endAt.time * 1_000_000 }
151151
.sortedBy { it.timestampNs }.toMutableList()

library/src/main/java/org/xmtp/android/library/ApiClient.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ data class GRPCApiClient(
6363
if (pagination != null) {
6464
it.pagingInfo = pagination.pagingInfo
6565
}
66-
if (pagination?.startTime != null) {
67-
it.endTimeNs = pagination.startTime.time * 1_000_000
66+
if (pagination?.before != null) {
67+
it.endTimeNs = pagination.before.time * 1_000_000
6868
it.pagingInfo = it.pagingInfo.toBuilder().also { info ->
6969
info.direction =
7070
MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING
7171
}.build()
7272
}
73-
if (pagination?.endTime != null) {
74-
it.startTimeNs = pagination.endTime.time * 1_000_000
73+
if (pagination?.after != null) {
74+
it.startTimeNs = pagination.after.time * 1_000_000
7575
it.pagingInfo = it.pagingInfo.toBuilder().also { info ->
7676
info.direction =
7777
MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING

library/src/main/java/org/xmtp/android/library/Conversation.kt

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.xmtp.android.library
22

3+
import android.util.Log
34
import kotlinx.coroutines.flow.Flow
45
import org.xmtp.android.library.messages.Envelope
56
import java.util.Date
@@ -60,6 +61,15 @@ sealed class Conversation {
6061
}
6162
}
6263

64+
fun decodeOrNull(envelope: Envelope): DecodedMessage? {
65+
return try {
66+
decode(envelope)
67+
} catch (e: Exception) {
68+
Log.d("CONVERSATION", "discarding message that failed to decode", e)
69+
null
70+
}
71+
}
72+
6373
fun <T> prepareMessage(content: T, options: SendOptions? = null): PreparedMessage {
6474
return when (this) {
6575
is V1 -> {

library/src/main/java/org/xmtp/android/library/ConversationV1.kt

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.xmtp.android.library
22

3+
import android.util.Log
34
import kotlinx.coroutines.flow.Flow
45
import kotlinx.coroutines.flow.flow
56
import kotlinx.coroutines.runBlocking
@@ -41,13 +42,13 @@ data class ConversationV1(
4142
before: Date? = null,
4243
after: Date? = null,
4344
): List<DecodedMessage> {
44-
val pagination = Pagination(limit = limit, startTime = before, endTime = after)
45+
val pagination = Pagination(limit = limit, before = before, after = after)
4546
val result = runBlocking {
4647
client.apiClient.queryTopic(topic = topic, pagination = pagination)
4748
}
4849

49-
return result.envelopesList.flatMap { envelope ->
50-
listOf(decode(envelope = envelope))
50+
return result.envelopesList.mapNotNull { envelope ->
51+
decodeOrNull(envelope = envelope)
5152
}
5253
}
5354

@@ -67,6 +68,15 @@ data class ConversationV1(
6768
return decoded
6869
}
6970

71+
private fun decodeOrNull(envelope: Envelope): DecodedMessage? {
72+
return try {
73+
decode(envelope)
74+
} catch (e: Exception) {
75+
Log.d("CONV_V1", "discarding message that failed to decode", e)
76+
null
77+
}
78+
}
79+
7080
fun send(text: String, options: SendOptions? = null): String {
7181
return send(text = text, sendOptions = options, sentAt = null)
7282
}

library/src/main/java/org/xmtp/android/library/ConversationV2.kt

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.xmtp.android.library
22

3+
import android.util.Log
34
import kotlinx.coroutines.flow.Flow
45
import kotlinx.coroutines.flow.flow
6+
import kotlinx.coroutines.flow.mapNotNull
57
import kotlinx.coroutines.runBlocking
68
import org.web3j.crypto.Hash
79
import org.xmtp.android.library.codecs.ContentCodec
@@ -58,7 +60,7 @@ data class ConversationV2(
5860
before: Date? = null,
5961
after: Date? = null,
6062
): List<DecodedMessage> {
61-
val pagination = Pagination(limit = limit, startTime = before, endTime = after)
63+
val pagination = Pagination(limit = limit, before = before, after = after)
6264
val result = runBlocking {
6365
client.apiClient.query(
6466
topic = topic,
@@ -67,14 +69,14 @@ data class ConversationV2(
6769
)
6870
}
6971

70-
return result.envelopesList.flatMap { envelope ->
71-
listOf(decodeEnvelope(envelope))
72+
return result.envelopesList.mapNotNull { envelope ->
73+
decodeEnvelopeOrNull(envelope)
7274
}
7375
}
7476

7577
fun streamMessages(): Flow<DecodedMessage> = flow {
76-
client.subscribe(listOf(topic)).collect {
77-
emit(decodeEnvelope(envelope = it))
78+
client.subscribe(listOf(topic)).mapNotNull { decodeEnvelopeOrNull(envelope = it) }.collect {
79+
emit(it)
7880
}
7981
}
8082

@@ -85,6 +87,15 @@ data class ConversationV2(
8587
return decoded
8688
}
8789

90+
private fun decodeEnvelopeOrNull(envelope: Envelope): DecodedMessage? {
91+
return try {
92+
decodeEnvelope(envelope)
93+
} catch (e: Exception) {
94+
Log.d("CONV_V2", "discarding message that failed to decode", e)
95+
null
96+
}
97+
}
98+
8899
fun decode(message: MessageV2): DecodedMessage =
89100
MessageV2Builder.buildDecode(message, keyMaterial = keyMaterial, topic = topic)
90101

0 commit comments

Comments
 (0)