feat: migrate nodeAliasCache to room

This commit is contained in:
2026-06-01 23:59:42 +02:00
parent 38c6c6f723
commit 93332dc683
12 changed files with 166 additions and 180 deletions
@@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.StateFlow
class LNbitsNodeAliasRepository(
private val service: NodeAliasService
) : NodeAliasRepository {
override val aliases: StateFlow<Map<String, String>>
get() = service.aliases
override suspend fun resolve(pubkey: String): String? =
service.resolve(pubkey)
override suspend fun warmUp() = service.warmUp()
}
@@ -1,10 +1,8 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.net.Uri
import android.util.Log
import com.bitcointxoko.gudariwallet.api.*
import com.bitcointxoko.gudariwallet.api.LNbitsClient.httpClient
import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.LnurlBech32
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
@@ -15,9 +13,6 @@ private const val TAG = "LNbitsWalletRepo"
class LNbitsWalletRepository(
private val api: LNbitsApi,
private val secrets: SecretStore,
private val context : Context,
private val nodeAliasService: com.bitcointxoko.gudariwallet.service.NodeAliasService =
com.bitcointxoko.gudariwallet.service.NodeAliasService(httpClient, context),
) : WalletRepository {
// ── Balance ───────────────────────────────────────────────────────────────
@@ -157,7 +152,7 @@ class LNbitsWalletRepository(
append(amountMsat)
if (!comment.isNullOrBlank()) {
append("&comment=")
append(android.net.Uri.encode(comment))
append(Uri.encode(comment))
}
}
Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl")
@@ -0,0 +1,68 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
import com.bitcointxoko.gudariwallet.data.db.NodeAliasCacheEntity
import com.bitcointxoko.gudariwallet.util.WalletConstants
private const val TAG = "NodeAliasCacheRepository"
class NodeAliasCacheRepository(context: Context) {
private val dao = AppDatabase.getInstance(context).nodeAliasCacheDao()
/**
* Returns a cached alias for [pubkey] if it exists and is within
* [WalletConstants.ALIAS_CACHE_TTL_MS]. Returns null on miss or stale.
*/
suspend fun get(pubkey: String): String? {
val entry = dao.getByPubkey(pubkey) ?: run {
Log.d(TAG, "ALIAS [CACHE MISS ] $pubkey")
return null
}
val ageMs = System.currentTimeMillis() - entry.savedAt
return if (ageMs < WalletConstants.ALIAS_CACHE_TTL_MS) {
Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey\"${entry.alias}\" (age ${ageMs / 1000}s)")
entry.alias
} else {
Log.d(TAG, "ALIAS [CACHE STALE] $pubkey (age ${ageMs / 1000}s) — will refetch")
null
}
}
/** Upsert an alias into the Room cache. */
suspend fun put(pubkey: String, alias: String) {
dao.insert(
NodeAliasCacheEntity(
pubkey = pubkey,
alias = alias,
savedAt = System.currentTimeMillis()
)
)
Log.d(TAG, "ALIAS [PERSIST ] $pubkey\"$alias\"")
}
/**
* Load all non-stale entries from Room as a plain map.
* Used to seed the StateFlow on startup — replaces [loadPersistedCache].
*/
suspend fun loadAll(): Map<String, String> {
val now = System.currentTimeMillis()
return dao.getAll()
.filter { now - it.savedAt < WalletConstants.ALIAS_PERSIST_TTL_MS }
.associate { it.pubkey to it.alias }
.also { Log.d(TAG, "ALIAS [PERSIST ] Loaded ${it.size} non-stale entries from Room") }
}
/** Remove a single entry. */
suspend fun remove(pubkey: String) {
dao.deleteByPubkey(pubkey)
}
/** Wipe the entire alias cache — mirrors [clearPersistedCache]. */
suspend fun clearAll() {
dao.deleteAll()
Log.d(TAG, "ALIAS [PERSIST ] Cache cleared")
}
}
@@ -18,4 +18,5 @@ interface NodeAliasRepository {
* Results are cached — repeated calls for the same pubkey are cheap.
*/
suspend fun resolve(pubkey: String): String?
suspend fun warmUp()
}
@@ -9,8 +9,12 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [PaymentRecordEntity::class, LnurlCacheEntity::class],
version = 2,
entities = [
PaymentRecordEntity::class,
LnurlCacheEntity::class,
NodeAliasCacheEntity::class, // ← added
],
version = 3, // ← bumped
exportSchema = false
)
@TypeConverters(PaymentConverters::class)
@@ -18,6 +22,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun paymentDao(): PaymentDao
abstract fun lnurlCacheDao(): LnurlCacheDao
abstract fun nodeAliasCacheDao(): NodeAliasCacheDao // ← added
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
@@ -37,6 +42,21 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) { // ← added
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `node_alias_cache` (
`pubkey` TEXT NOT NULL,
`alias` TEXT NOT NULL,
`savedAt` INTEGER NOT NULL,
PRIMARY KEY(`pubkey`)
)
""".trimIndent()
)
}
}
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
@@ -44,7 +64,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
"gudari_wallet.db"
)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // ← added
.build()
.also { INSTANCE = it }
}
@@ -0,0 +1,25 @@
package com.bitcointxoko.gudariwallet.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface NodeAliasCacheDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entry: NodeAliasCacheEntity)
@Query("SELECT * FROM node_alias_cache WHERE pubkey = :pubkey LIMIT 1")
suspend fun getByPubkey(pubkey: String): NodeAliasCacheEntity?
@Query("SELECT * FROM node_alias_cache")
suspend fun getAll(): List<NodeAliasCacheEntity>
@Query("DELETE FROM node_alias_cache WHERE pubkey = :pubkey")
suspend fun deleteByPubkey(pubkey: String)
@Query("DELETE FROM node_alias_cache")
suspend fun deleteAll()
}
@@ -0,0 +1,12 @@
package com.bitcointxoko.gudariwallet.data.db
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "node_alias_cache")
data class NodeAliasCacheEntity(
@PrimaryKey
val pubkey: String, // node public key
val alias: String, // resolved human-readable alias
val savedAt: Long // epoch ms — used for TTL checks
)