Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion app/src/main/java/com/pombo/android/core/EnsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
Expand Down Expand Up @@ -39,8 +41,20 @@ class EnsStore(context: Context) {
val avatarUrls: StateFlow<Map<String, String>> = _avatarUrls.asStateFlow()

private val file = File(context.filesDir, "ens-cache.json")
private val loadLock = Mutex()
@Volatile private var loaded = false

/** Reads the disk cache, once; [name] and [avatar] wait for it. */
suspend fun warmUp() {
if (loaded) return
loadLock.withLock {
if (loaded) return
readDisk()
loaded = true
}
}

suspend fun warmUp() = withContext(Dispatchers.IO) {
private suspend fun readDisk() = withContext(Dispatchers.IO) {
try {
if (!file.exists()) return@withContext
val root = JSONObject(file.readText())
Expand Down Expand Up @@ -92,6 +106,7 @@ class EnsStore(context: Context) {
* cold or stale. Concurrent callers share one lookup.
*/
suspend fun name(address: String, lookup: suspend () -> String?): String? {
warmUp()
val key = address.lowercase()
names[key]?.let { if (fresh(it)) return it.value }

Expand All @@ -117,6 +132,7 @@ class EnsStore(context: Context) {

/** Same contract for the avatar text record. */
suspend fun avatar(address: String, lookup: suspend () -> String?): String? {
warmUp()
val key = address.lowercase()
avatars[key]?.let { if (fresh(it)) return it.value }

Expand Down
28 changes: 28 additions & 0 deletions app/src/test/java/com/pombo/android/core/EnsStoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.pombo.android.core
import android.content.Context
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -45,4 +48,29 @@ class EnsStoreTest {
val store = EnsStore(context).apply { warmUp() }
assertEquals("found.eth", store.name("0xabc") { "found.eth" })
}

@Test
fun `a lookup made before warmUp sees what is on disk`() = runBlocking {
storeOnDisk(JSONObject().put("name", "on.disk.eth").put("at", twentyMinutesAgo))
var lookups = 0
assertEquals("on.disk.eth", EnsStore(context).name("0xabc") { lookups++; "net.eth" })
assertEquals(0, lookups)
}

@Test
fun `concurrent first lookups all wait for the disk`() = runBlocking {
storeOnDisk(
JSONObject().put("name", JSONObject.NULL).put("nameConfirmed", true).put("at", twentyMinutesAgo)
.put("avatar", "https://a.example/a.png").put("avatarAt", twentyMinutesAgo)
)
val store = EnsStore(context)
var lookups = 0
val results = listOf(
async(Dispatchers.Default) { store.name("0xabc") { lookups++; "net.eth" } },
async(Dispatchers.Default) { store.name("0xABC") { lookups++; "net.eth" } },
async(Dispatchers.Default) { store.avatar("0xabc") { lookups++; "https://net/a.png" } }
).awaitAll()
assertEquals(listOf(null, null, "https://a.example/a.png"), results)
assertEquals(0, lookups)
}
}
Loading