60edd473c7
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20). - Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references. - Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching. - Removes compose-desktop.md (superseded by desktop-expert/). - Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption, large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing, android-expert/image-loading. - New skills: account-state (Account + LocalCache), relay-client (subscriptions, filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family), auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
3.2 KiB
3.2 KiB
LargeCache: Platform-Aware In-Memory Store
quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/cache/LargeCache.kt provides a thread-safe key-value cache with a functional iteration API. Used everywhere Amethyst needs to hold many events, users, or derived state in memory.
Files
LargeCache.kt—expect class LargeCache<K, V>and its factorycreateLargeCache().ICacheOperations.kt— interface the cache exposes:forEach,filter,map,mapNotNull,groupBy,maxOrNullOf,sumOf,count,any,firstOrNull, etc.CacheCollectors.kt— functional collector helpers used by the cache API.
Actual implementations
- Android (
androidMain) — backed by aConcurrentHashMap(and optionallyandroidx.collection.LruCachevariants for size-bounded caches). - JVM/Desktop (
jvmMain) —ConcurrentHashMapdirectly. - iOS (
iosMain) —NSMapTable/Kotlin concurrent map wrapper.
Core API
val cache: LargeCache<HexKey, Note> = LargeCache()
cache.put(id, note)
cache.get(id) // V?
cache.getOrCreate(id) { Note(id) } // atomic compute-if-absent
cache.containsKey(id)
cache.remove(id)
cache.size()
// Functional iteration — thread-safe snapshot semantics
cache.forEach { key, value -> ... }
cache.filter { key, value -> value.kind == 1 }
cache.map { key, value -> value.pubKey }
cache.count { _, v -> v.isUnread }
cache.maxOrNullOf { _, v -> v.createdAt }
cache.groupBy { _, v -> v.kind }
The important contract: functional operations iterate a consistent snapshot, so you can filter inside a coroutine without racing concurrent writers. This is why LocalCache (the Amethyst event store) can be scanned to build a feed while relays are still inserting.
When to Use
- Event / note stores —
LocalCache.notes: LargeCache<HexKey, Note>. - User profiles —
LocalCache.users: LargeCache<HexKey, User>. - Address → event lookups for addressable (parameterized replaceable) events.
- Shared-secret caches (see
SharedKeyCache.kt— a similar pattern at smaller scale).
When Not to Use
- Small maps (<100 entries) — regular
mutableMapOfis fine. - Off-process state (DB, disk) — use the
store/event DB, not LargeCache. - Hot one-shot lookups — if you're already inside a Flow pipeline, chain operators rather than maintaining a parallel cache.
Gotchas
getOrCreatevsput—getOrCreateis atomic and safe under contention;getthenputis a race.- Iteration during mutation is safe but the snapshot may include or exclude a concurrent write. Don't rely on a just-put value being visible inside a currently-running
forEach. - Don't store
Flows inside LargeCache. Cache values should be immutable / thread-safe objects. For reactive state, keep aStateFlownext to the cache and emit on writes. - No TTL / eviction by default. If you need bounded size, wrap with
LruCacheor build an explicit eviction loop keyed off a secondary structure.
Related
amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt— the canonical user ofLargeCache<HexKey, Note>andLargeCache<HexKey, User>.nip44Encryption/SharedKeyCache.kt— smaller domain-specific cache using the same pattern.