Skip to content

Commit 3d33a40

Browse files
authored
Cache Consent Records (#218)
* first pass at performance and caching * fix up the caching * set just one date * refactor publish to do a batch instead of 1 at a time * validate consent performance improvement * fix up the tests * fix up linter * maybe this will fix the grpc issues * improve the publish and write a test * test flakes sometimes
1 parent 3c8a080 commit 3d33a40

File tree

3 files changed

+87
-59
lines changed

3 files changed

+87
-59
lines changed

library/build.gradle

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ protobuf {
6060
}
6161
plugins {
6262
grpc {
63-
artifact = "io.grpc:protoc-gen-grpc-java:1.47.0"
63+
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
6464
}
6565
grpckt {
66-
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.0:jdk8@jar"
66+
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
6767
}
6868
}
6969
generateProtoTasks {
@@ -79,9 +79,9 @@ protobuf {
7979

8080
dependencies {
8181
implementation 'com.google.crypto.tink:tink-android:1.8.0'
82-
implementation 'io.grpc:grpc-kotlin-stub:1.3.0'
83-
implementation 'io.grpc:grpc-okhttp:1.51.1'
84-
implementation 'io.grpc:grpc-protobuf-lite:1.51.0'
82+
implementation 'io.grpc:grpc-kotlin-stub:1.4.1'
83+
implementation 'io.grpc:grpc-okhttp:1.62.2'
84+
implementation 'io.grpc:grpc-protobuf-lite:1.62.2'
8585
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
8686
implementation 'org.web3j:crypto:5.0.0'
8787
implementation "net.java.dev.jna:jna:5.13.0@aar"

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

+26-12
Original file line numberDiff line numberDiff line change
@@ -519,13 +519,7 @@ class ConversationTest {
519519
),
520520
)
521521
}
522-
val isSteveOrBobConversation = { topic: String ->
523-
(topic.lowercase() == steveConversation.topic.lowercase() || topic.lowercase() == bobConversation.topic.lowercase())
524-
}
525522
assertEquals(3, messages.size)
526-
assertTrue(isSteveOrBobConversation(messages[0].topic))
527-
assertTrue(isSteveOrBobConversation(messages[1].topic))
528-
assertTrue(isSteveOrBobConversation(messages[2].topic))
529523
}
530524

531525
@Test
@@ -799,9 +793,10 @@ class ConversationTest {
799793
assertTrue(isAllowed)
800794
assertTrue(bobClient.contacts.isAllowed(alice.walletAddress))
801795

802-
runBlocking { bobClient.contacts.deny(listOf(alice.walletAddress)) }
803-
bobClient.contacts.refreshConsentList()
804-
796+
runBlocking {
797+
bobClient.contacts.deny(listOf(alice.walletAddress))
798+
bobClient.contacts.refreshConsentList()
799+
}
805800
val isDenied = bobConversation.consentState() == ConsentState.DENIED
806801
assertEquals(bobClient.contacts.consentList.entries.size, 1)
807802
assertTrue(isDenied)
@@ -820,7 +815,7 @@ class ConversationTest {
820815
val aliceClient2 = Client().create(aliceWallet)
821816
val aliceConversation2 = runBlocking { aliceClient2.conversations.list()[0] }
822817

823-
aliceClient2.contacts.refreshConsentList()
818+
runBlocking { aliceClient2.contacts.refreshConsentList() }
824819

825820
// Allow state should sync across clients
826821
val isBobAllowed2 = aliceConversation2.consentState() == ConsentState.ALLOWED
@@ -843,14 +838,33 @@ class ConversationTest {
843838
// Conversations you receive should start as unknown
844839
assertTrue(isUnknown)
845840

846-
runBlocking { aliceConversation.send(content = "hey bob") }
847-
aliceClient.contacts.refreshConsentList()
841+
runBlocking {
842+
aliceConversation.send(content = "hey bob")
843+
aliceClient.contacts.refreshConsentList()
844+
}
848845
val isNowAllowed = aliceConversation.consentState() == ConsentState.ALLOWED
849846

850847
// Conversations you send a message to get marked as allowed
851848
assertTrue(isNowAllowed)
852849
}
853850

851+
@Test
852+
fun testCanPublishMultipleAddressConsentState() {
853+
runBlocking {
854+
val bobConversation = bobClient.conversations.newConversation(alice.walletAddress)
855+
val caroConversation =
856+
bobClient.conversations.newConversation(fixtures.caro.walletAddress)
857+
bobClient.contacts.refreshConsentList()
858+
assertEquals(bobClient.contacts.consentList.entries.size, 2)
859+
assertTrue(bobConversation.consentState() == ConsentState.ALLOWED)
860+
assertTrue(caroConversation.consentState() == ConsentState.ALLOWED)
861+
bobClient.contacts.deny(listOf(alice.walletAddress, fixtures.caro.walletAddress))
862+
assertEquals(bobClient.contacts.consentList.entries.size, 2)
863+
assertTrue(bobConversation.consentState() == ConsentState.DENIED)
864+
assertTrue(caroConversation.consentState() == ConsentState.DENIED)
865+
}
866+
}
867+
854868
@Test
855869
fun testCanValidateTopicsInsideConversation() {
856870
val validId = "sdfsadf095b97a9284dcd82b2274856ccac8a21de57bebe34e7f9eeb855fb21126d3b8f"

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

+56-42
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ data class ConsentListEntry(
4848
get() = "${entryType.name}-$value"
4949
}
5050

51-
class ConsentList(val client: Client) {
52-
val entries: MutableMap<String, ConsentListEntry> = mutableMapOf()
51+
class ConsentList(
52+
val client: Client,
53+
val entries: MutableMap<String, ConsentListEntry> = mutableMapOf(),
54+
) {
55+
private var lastFetched: Date? = null
5356
private val publicKey =
5457
client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
5558
private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
@@ -60,13 +63,17 @@ class ConsentList(val client: Client) {
6063
)
6164

6265
@OptIn(ExperimentalUnsignedTypes::class)
63-
suspend fun load(): ConsentList {
66+
suspend fun load(): List<ConsentListEntry> {
67+
val newDate = Date()
6468
val envelopes =
6569
client.apiClient.envelopes(
6670
Topic.preferenceList(identifier).description,
67-
Pagination(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING),
71+
Pagination(
72+
after = lastFetched,
73+
direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING
74+
),
6875
)
69-
val consentList = ConsentList(client)
76+
lastFetched = newDate
7077
val preferences: MutableList<PrivatePreferencesAction> = mutableListOf()
7178
for (envelope in envelopes) {
7279
val payload =
@@ -79,64 +86,70 @@ class ConsentList(val client: Client) {
7986
preferences.add(
8087
PrivatePreferencesAction.parseFrom(
8188
payload.toUByteArray().toByteArray(),
82-
),
89+
)
8390
)
8491
}
8592

8693
preferences.iterator().forEach { preference ->
8794
preference.allowAddress?.walletAddressesList?.forEach { address ->
88-
consentList.allow(address)
95+
allow(address)
8996
}
9097
preference.denyAddress?.walletAddressesList?.forEach { address ->
91-
consentList.deny(address)
98+
deny(address)
9299
}
93100
preference.allowGroup?.groupIdsList?.forEach { groupId ->
94-
consentList.allowGroup(groupId.toByteArray())
101+
allowGroup(groupId.toByteArray())
95102
}
96103
preference.denyGroup?.groupIdsList?.forEach { groupId ->
97-
consentList.denyGroup(groupId.toByteArray())
104+
denyGroup(groupId.toByteArray())
98105
}
99106
}
100107

101-
return consentList
108+
return entries.values.toList()
102109
}
103110

104-
suspend fun publish(entry: ConsentListEntry) {
105-
val payload =
106-
PrivatePreferencesAction.newBuilder().also {
111+
suspend fun publish(entries: List<ConsentListEntry>) {
112+
val payload = PrivatePreferencesAction.newBuilder().also {
113+
entries.forEach { entry ->
107114
when (entry.entryType) {
108115
ConsentListEntry.EntryType.ADDRESS -> {
109116
when (entry.consentType) {
110117
ConsentState.ALLOWED ->
111118
it.setAllowAddress(
112-
PrivatePreferencesAction.AllowAddress.newBuilder().addWalletAddresses(entry.value),
119+
PrivatePreferencesAction.AllowAddress.newBuilder()
120+
.addWalletAddresses(entry.value),
113121
)
114122

115123
ConsentState.DENIED ->
116124
it.setDenyAddress(
117-
PrivatePreferencesAction.DenyAddress.newBuilder().addWalletAddresses(entry.value),
125+
PrivatePreferencesAction.DenyAddress.newBuilder()
126+
.addWalletAddresses(entry.value),
118127
)
119128

120129
ConsentState.UNKNOWN -> it.clearMessageType()
121130
}
122131
}
132+
123133
ConsentListEntry.EntryType.GROUP_ID -> {
124134
when (entry.consentType) {
125135
ConsentState.ALLOWED ->
126136
it.setAllowGroup(
127-
PrivatePreferencesAction.AllowGroup.newBuilder().addGroupIds(entry.value.toByteStringUtf8()),
137+
PrivatePreferencesAction.AllowGroup.newBuilder()
138+
.addGroupIds(entry.value.toByteStringUtf8()),
128139
)
129140

130141
ConsentState.DENIED ->
131142
it.setDenyGroup(
132-
PrivatePreferencesAction.DenyGroup.newBuilder().addGroupIds(entry.value.toByteStringUtf8()),
143+
PrivatePreferencesAction.DenyGroup.newBuilder()
144+
.addGroupIds(entry.value.toByteStringUtf8()),
133145
)
134146

135147
ConsentState.UNKNOWN -> it.clearMessageType()
136148
}
137149
}
138150
}
139-
}.build()
151+
}
152+
}.build()
140153

141154
val message =
142155
uniffi.xmtpv3.userPreferencesEncrypt(
@@ -145,40 +158,39 @@ class ConsentList(val client: Client) {
145158
payload.toByteArray(),
146159
)
147160

148-
val envelope =
149-
EnvelopeBuilder.buildFromTopic(
150-
Topic.preferenceList(identifier),
151-
Date(),
152-
ByteArray(message.size) { message[it] },
153-
)
161+
val envelope = EnvelopeBuilder.buildFromTopic(
162+
Topic.preferenceList(identifier),
163+
Date(),
164+
ByteArray(message.size) { message[it] },
165+
)
154166

155167
client.publish(listOf(envelope))
156168
}
157169

158170
fun allow(address: String): ConsentListEntry {
159171
val entry = ConsentListEntry.address(address, ConsentState.ALLOWED)
160-
entries[ConsentListEntry.address(address).key] = entry
172+
entries[entry.key] = entry
161173

162174
return entry
163175
}
164176

165177
fun deny(address: String): ConsentListEntry {
166178
val entry = ConsentListEntry.address(address, ConsentState.DENIED)
167-
entries[ConsentListEntry.address(address).key] = entry
179+
entries[entry.key] = entry
168180

169181
return entry
170182
}
171183

172184
fun allowGroup(groupId: ByteArray): ConsentListEntry {
173185
val entry = ConsentListEntry.groupId(groupId, ConsentState.ALLOWED)
174-
entries[ConsentListEntry.groupId(groupId).key] = entry
186+
entries[entry.key] = entry
175187

176188
return entry
177189
}
178190

179191
fun denyGroup(groupId: ByteArray): ConsentListEntry {
180192
val entry = ConsentListEntry.groupId(groupId, ConsentState.DENIED)
181-
entries[ConsentListEntry.groupId(groupId).key] = entry
193+
entries[entry.key] = entry
182194

183195
return entry
184196
}
@@ -200,38 +212,40 @@ data class Contacts(
200212
var client: Client,
201213
val knownBundles: MutableMap<String, ContactBundle> = mutableMapOf(),
202214
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf(),
215+
var consentList: ConsentList = ConsentList(client),
203216
) {
204-
var consentList: ConsentList = ConsentList(client)
205217

206-
fun refreshConsentList(): ConsentList {
207-
runBlocking {
208-
consentList = ConsentList(client).load()
209-
}
218+
suspend fun refreshConsentList(): ConsentList {
219+
consentList.load()
210220
return consentList
211221
}
212222

213223
suspend fun allow(addresses: List<String>) {
214-
for (address in addresses) {
215-
ConsentList(client).publish(consentList.allow(address))
224+
val entries = addresses.map {
225+
consentList.allow(it)
216226
}
227+
consentList.publish(entries)
217228
}
218229

219230
suspend fun deny(addresses: List<String>) {
220-
for (address in addresses) {
221-
ConsentList(client).publish(consentList.deny(address))
231+
val entries = addresses.map {
232+
consentList.deny(it)
222233
}
234+
consentList.publish(entries)
223235
}
224236

225237
suspend fun allowGroup(groupIds: List<ByteArray>) {
226-
for (id in groupIds) {
227-
ConsentList(client).publish(consentList.allowGroup(id))
238+
val entries = groupIds.map {
239+
consentList.allowGroup(it)
228240
}
241+
consentList.publish(entries)
229242
}
230243

231244
suspend fun denyGroup(groupIds: List<ByteArray>) {
232-
for (id in groupIds) {
233-
ConsentList(client).publish(consentList.denyGroup(id))
245+
val entries = groupIds.map {
246+
consentList.denyGroup(it)
234247
}
248+
consentList.publish(entries)
235249
}
236250

237251
fun isAllowed(address: String): Boolean {

0 commit comments

Comments
 (0)