perf(multi-account): cache metadata and encryption key in memory

DesktopAccountStorage now caches AccountMetadata in memory after first
read. Subsequent loadAccounts/saveAccount/setCurrentAccount serve from
cache, only hitting disk on writes. Also caches AES encryption key to
avoid repeated OS keychain lookups.

Before: 3 decrypts + 1 encrypt + 4 keychain calls per login/switch
After: 1 decrypt + 1 keychain call on first access, then memory only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-29 10:07:56 +03:00
parent 5a1b9d445c
commit 4c832c008c
@@ -62,19 +62,22 @@ class DesktopAccountStorage(
private val mapper = jacksonObjectMapper()
private val amethystDir by lazy { File(homeDir, ".amethyst") }
// In-memory cache — read from disk once, then serve from memory
private var cachedMetadata: AccountMetadata? = null
// --- AccountStorage interface ---
override suspend fun loadAccounts(): List<AccountInfo> = readMetadata().accounts.map { it.toAccountInfo() }
override suspend fun loadAccounts(): List<AccountInfo> = getCachedMetadata().accounts.map { it.toAccountInfo() }
override suspend fun saveAccount(info: AccountInfo) {
val metadata = readMetadata()
val metadata = getCachedMetadata()
val dto = AccountInfoDto.from(info)
val updated = metadata.accounts.filter { it.npub != info.npub } + dto
writeMetadata(metadata.copy(accounts = updated))
writeCachedMetadata(metadata.copy(accounts = updated))
}
override suspend fun deleteAccount(npub: String) {
val metadata = readMetadata()
val metadata = getCachedMetadata()
val updated = metadata.accounts.filter { it.npub != npub }
val newActive =
if (metadata.activeNpub == npub) {
@@ -82,19 +85,33 @@ class DesktopAccountStorage(
} else {
metadata.activeNpub
}
writeMetadata(metadata.copy(accounts = updated, activeNpub = newActive))
writeCachedMetadata(metadata.copy(accounts = updated, activeNpub = newActive))
}
override suspend fun currentAccount(): String? = readMetadata().activeNpub
override suspend fun currentAccount(): String? = getCachedMetadata().activeNpub
override suspend fun setCurrentAccount(npub: String) {
val metadata = readMetadata()
writeMetadata(metadata.copy(activeNpub = npub))
val metadata = getCachedMetadata()
writeCachedMetadata(metadata.copy(activeNpub = npub))
}
// --- Cached I/O ---
private suspend fun getCachedMetadata(): AccountMetadata {
cachedMetadata?.let { return it }
val loaded = readMetadataFromDisk()
cachedMetadata = loaded
return loaded
}
private suspend fun writeCachedMetadata(metadata: AccountMetadata) {
cachedMetadata = metadata
writeMetadataToDisk(metadata)
}
// --- Encrypted file I/O ---
private suspend fun readMetadata(): AccountMetadata {
private suspend fun readMetadataFromDisk(): AccountMetadata {
val file = getAccountsFile()
if (!file.exists()) return AccountMetadata()
@@ -108,7 +125,7 @@ class DesktopAccountStorage(
}
}
private suspend fun writeMetadata(metadata: AccountMetadata) {
private suspend fun writeMetadataToDisk(metadata: AccountMetadata) {
ensureDir()
val json = mapper.writeValueAsBytes(metadata)
val encrypted = encrypt(json)
@@ -126,12 +143,21 @@ class DesktopAccountStorage(
// --- AES-256-GCM encryption ---
private var cachedKey: ByteArray? = null
private suspend fun getOrCreateKey(): ByteArray {
cachedKey?.let { return it }
val existing = secureStorage.getPrivateKey(METADATA_KEY_ALIAS)
if (existing != null) return Base64.getDecoder().decode(existing)
if (existing != null) {
val key = Base64.getDecoder().decode(existing)
cachedKey = key
return key
}
val key = ByteArray(AES_KEY_SIZE).also { SecureRandom().nextBytes(it) }
secureStorage.savePrivateKey(METADATA_KEY_ALIAS, Base64.getEncoder().encodeToString(key))
cachedKey = key
return key
}