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:
+37
-11
@@ -62,19 +62,22 @@ class DesktopAccountStorage(
|
|||||||
private val mapper = jacksonObjectMapper()
|
private val mapper = jacksonObjectMapper()
|
||||||
private val amethystDir by lazy { File(homeDir, ".amethyst") }
|
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 ---
|
// --- 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) {
|
override suspend fun saveAccount(info: AccountInfo) {
|
||||||
val metadata = readMetadata()
|
val metadata = getCachedMetadata()
|
||||||
val dto = AccountInfoDto.from(info)
|
val dto = AccountInfoDto.from(info)
|
||||||
val updated = metadata.accounts.filter { it.npub != info.npub } + dto
|
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) {
|
override suspend fun deleteAccount(npub: String) {
|
||||||
val metadata = readMetadata()
|
val metadata = getCachedMetadata()
|
||||||
val updated = metadata.accounts.filter { it.npub != npub }
|
val updated = metadata.accounts.filter { it.npub != npub }
|
||||||
val newActive =
|
val newActive =
|
||||||
if (metadata.activeNpub == npub) {
|
if (metadata.activeNpub == npub) {
|
||||||
@@ -82,19 +85,33 @@ class DesktopAccountStorage(
|
|||||||
} else {
|
} else {
|
||||||
metadata.activeNpub
|
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) {
|
override suspend fun setCurrentAccount(npub: String) {
|
||||||
val metadata = readMetadata()
|
val metadata = getCachedMetadata()
|
||||||
writeMetadata(metadata.copy(activeNpub = npub))
|
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 ---
|
// --- Encrypted file I/O ---
|
||||||
|
|
||||||
private suspend fun readMetadata(): AccountMetadata {
|
private suspend fun readMetadataFromDisk(): AccountMetadata {
|
||||||
val file = getAccountsFile()
|
val file = getAccountsFile()
|
||||||
if (!file.exists()) return AccountMetadata()
|
if (!file.exists()) return AccountMetadata()
|
||||||
|
|
||||||
@@ -108,7 +125,7 @@ class DesktopAccountStorage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeMetadata(metadata: AccountMetadata) {
|
private suspend fun writeMetadataToDisk(metadata: AccountMetadata) {
|
||||||
ensureDir()
|
ensureDir()
|
||||||
val json = mapper.writeValueAsBytes(metadata)
|
val json = mapper.writeValueAsBytes(metadata)
|
||||||
val encrypted = encrypt(json)
|
val encrypted = encrypt(json)
|
||||||
@@ -126,12 +143,21 @@ class DesktopAccountStorage(
|
|||||||
|
|
||||||
// --- AES-256-GCM encryption ---
|
// --- AES-256-GCM encryption ---
|
||||||
|
|
||||||
|
private var cachedKey: ByteArray? = null
|
||||||
|
|
||||||
private suspend fun getOrCreateKey(): ByteArray {
|
private suspend fun getOrCreateKey(): ByteArray {
|
||||||
|
cachedKey?.let { return it }
|
||||||
|
|
||||||
val existing = secureStorage.getPrivateKey(METADATA_KEY_ALIAS)
|
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) }
|
val key = ByteArray(AES_KEY_SIZE).also { SecureRandom().nextBytes(it) }
|
||||||
secureStorage.savePrivateKey(METADATA_KEY_ALIAS, Base64.getEncoder().encodeToString(key))
|
secureStorage.savePrivateKey(METADATA_KEY_ALIAS, Base64.getEncoder().encodeToString(key))
|
||||||
|
cachedKey = key
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user