From 0488ba33127ecd273d4c64e54d81e4b93afdf45d Mon Sep 17 00:00:00 2001 From: Ocnrb Date: Tue, 6 Oct 2026 17:25:27 +0100 Subject: [PATCH] Read the ENS cache from disk before the first lookup - The startup resolved the account's own name and every DM peer's before the disk cache was loaded, so each start asked the network again for names it already had; name() and avatar() now wait for the one disk read Co-authored-by: Claude Opus 5.5 --- .../java/com/pombo/android/core/EnsStore.kt | 18 +++++++++++- .../com/pombo/android/core/EnsStoreTest.kt | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/pombo/android/core/EnsStore.kt b/app/src/main/java/com/pombo/android/core/EnsStore.kt index eedbdd8..cbe928e 100644 --- a/app/src/main/java/com/pombo/android/core/EnsStore.kt +++ b/app/src/main/java/com/pombo/android/core/EnsStore.kt @@ -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 @@ -39,8 +41,20 @@ class EnsStore(context: Context) { val avatarUrls: StateFlow> = _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()) @@ -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 } @@ -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 } diff --git a/app/src/test/java/com/pombo/android/core/EnsStoreTest.kt b/app/src/test/java/com/pombo/android/core/EnsStoreTest.kt index 86142d9..c1fe0d6 100644 --- a/app/src/test/java/com/pombo/android/core/EnsStoreTest.kt +++ b/app/src/test/java/com/pombo/android/core/EnsStoreTest.kt @@ -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 @@ -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) + } }