Quartz moves to its own repo, the way the crypto primitives moved to crypto-kmp. Every module depends on com.vitorpamplona.quartz:quartz 2.0.0 (libs.quartz) instead of :quartz, and -PquartzDir=../quartz builds a local checkout instead of the artifact, for every module and for quic's own quartz dependency (which replaces the root build's substitution of the published quartz with :quartz). What moved with quartz: its CI jobs (lint tasks, test-quartz-linux-native, test-quartz-ios), the Maven Central publish step of the release workflow, jitpack.yml, tools/unicode-nfkc, the nostr-expert and event-store-semantics skills, the agent-skill check, and the README's library usage docs. What stays here: - The four quartz client tests that need a real Ktor socket relay (TrustNetworkSync*, GiantReqStreamTest, BulkDownloadBenchmark) move to geode's tests; geode forwards -PprodRelayBench for the latter. quartz's other relay-client tests now use a test relay of its own, which ends quartz's test dependency on geode. - The benchmark module's resources (previously read from quartz's androidDeviceTest resources) move into benchmark/; relayBench --real reads the corpus from there. - NoteComposeKindCoverageTest reads EventFactory.kt from quartz's JVM sources jar, or from the -PquartzDir checkout, passed by commonsUI's build. - commons and commonsUI declare crypto-kmp (and commons mls-kmp), which they import directly. Through quartz's api alone, a -PcryptoKmpDir checkout never reached the common metadata compile. - The catalog drops what only quartz used (kchesslib, negentropy, jackson-databind, sqlite-bundled, secp256k1-kmp common, compose runtime-annotation, the maven-publish plugin). agent_config_check checks a skill's quartz/... paths against a ../quartz checkout when there is one and skips them otherwise. AGENTS.md, the skills, CONTRIBUTING, BUILDING, RELEASE_OPS, PLANS and the READMEs point at the new repo. Needs quartz 2.0.0 (and crypto-kmp 1.0.0) on Maven Central before CI can resolve it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEkJaDWYrcTbGvT8JLQCrG
7.3 KiB
name: account-state description: Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow), FeedFilter/AdditiveFeedFilter, FeedViewModel. Use when adding an account setting or list, a feed, or choosing LocalCache vs account flows.
Account, LocalCache and Feeds
The client state backbone, all headless in commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/
(abbreviated commons/…/ below):
model/cache/LocalCache.kt—object LocalCache : EventCache(). EveryNote/Userthe client has seen. All logic is inmodel/cache/EventCache.kt(an open class so tests can build their own); Android and Desktop both use theLocalCachesingleton (AppModules.kt,DesktopAppModules.kt).model/Account.kt— oneAccountper signed-in user: ~50 per-feature state objects (kind3FollowList,nip65RelayList,muteList,bookmarkState, …), each exposing.flow.feeds/+viewmodels/+<feature>/dal/— feeds: aFeedFilterscansLocalCache, aFeedContentState/FeedViewModelkeeps the result live.
relay EVENT ─► EventCache.justConsume ─► Note/User updated, indexes, live.newNote(...)
│ │
▼ ▼
XState pins its addressable note, LocalCacheFlow.newEventBundles
note.flow().metadata → XState.flow → FeedViewModel → FeedFilter.applyFilter
│ │
└────────► ViewModels / composables ◄┘
Account state objects
Pattern, copied from model/nip51Lists/muteList/MuteListState.kt:
- Pin the note.
val muteListNote = cache.getOrCreateAddressableNote(MuteListEvent.createAddress(signer.pubKey)).LocalCache'snotes/addressablesareLargeSoftCache(weak references), so the state object's field is what keeps its note from being collected. - Expose one
StateFlow. MapmuteListNote.flow().metadata.stateFlow→ decrypt (private lists go through a per-feature…DecryptionCachesuch asMuteListDecryptionCache, held next to it inAccountasmuteListDecryptionCache) → fall back to theAccountSettingsbackup (settings.backupMuteList) when the event is absent →stateIn(scope, SharingStarted.Eagerly, …). - Mutation helpers return a signed event — publishing it is the caller's job.
MuteListState.hideUser(pubkey)returns aMuteListEvent;Account.hideUserdoessendMyPublicAndPrivateOutbox(muteList.hideUser(pubkeyHex)). A state change you never send is invisible to every other client and is lost when the next relay copy arrives. - Consumers read
account.<feature>.flow, never aMutableStateFlowonAccount.
Full catalog of properties, classes, kinds and packages: references/account-state-flow.md.
Adding an account-scoped setting or list
- Persisted as a Nostr event? Pick the kind (NIP-51 list, NIP-78 app data, a relay list) and make
sure quartz has the event class (see quartz's
nostr-expertskill; a new one is a quartz change, built here with-PquartzDir=../quartz). - Add
commons/…/model/nipXX…/XState.ktmodeled onMuteListState(encrypted) ornip51Lists/BookmarkListState.kt(plain): pin, flow, suspend mutation helpers that build the event with the quartz class and sign it. - Instantiate it as a
valinAccount.kt(plus itsDecryptionCacheif private) and add anAccountmethod that sends the returned event (sendMyPublicAndPrivateOutbox,sendAutomatic, …). If the list must survive relay loss, add abackupXfield toAccountSettings. - Make sure
EventCache.justConsumeInnerInnerlists the event class — a kind that is not listed falls into theelsebranch and is rejected, so the state object never sees it, even thoughEventFactoryparses it. Replaceable kinds without custom logic go in theconsumeBaseReplaceablegroup. - Relay subscription:
relay-client. UI: settings screens are shared incommonsUI/…/commons/ui/screen/loggedIn/settings/; strings follow the AGENTS.md Strings rules.
LocalCache vs Account flow vs feed
| You render… | Read |
|---|---|
| one note / user you have an id for | LocalCache.getOrCreateNote(id) / getOrCreateUser(pubkey), then note.flow().metadata (or replies, reactions, zaps, …) |
| "my follows / mutes / relays / bookmarks" | account.kind3FollowList.flow, account.muteList.flow, account.nip65RelayList.flow, account.hiddenUsers.flow, … |
| a list of notes | a FeedFilter + FeedContentState / FeedViewModel — references/feeds.md |
| ad-hoc filter results | LocalCache.observeNotes(filter: Filter): Flow<List<Note>> |
Never scan LocalCache.notes inside a composable.
Feeds in one paragraph
Bases are in commons/…/feeds/: IFeedFilter / FeedFilter<T> (feed(), feedKey(), limit()
default 500, showHiddenKey()), AdditiveFeedFilter<T> (adds applyFilter + sort for
incremental merges), AdditiveComplexFeedFilter<T, U> (incoming type differs from row type, e.g.
home/dal/HomeLiveFilter.kt), ChangesFlowFilter<T>, FilterByListParams, DefaultFeedOrder /
sortedByDefaultFeedOrder(), FeedContentState, FeedState. Concrete filters are shared, in
commons/…/<feature>/dal/ (home/dal/, hashtag/dal/, notifications/dal/, ~55 of them). The
account's main-tab feeds are long-lived FeedContentStates in viewmodels/AccountFeedContentStates.kt
(owned by BaseAccountViewModel.feedStates); per-screen feeds use a FeedViewModel subclass. Recipe,
real code and traps: references/feeds.md.
Gotchas
LocalCacheis shared across accounts. Switching accounts doesn't wipe it; everyAccountderives its flows from the same cache. Filters that depend on the account must put the account pubkey infeedKey().- Hold the
Note, not theEvent.getOrCreate*returns the same wrapper per id; for replaceable/addressable notesnote.eventis swapped in place when a newer version arrives. - Flows on a
Noteare not durable.CachePruner(model/cache/CachePruner.kt) callsclearFlow()on notes during cleanup, and anything not strongly held can be collected. Re-fetch vianote.flow(); don't stash flow references. - Observers can fire before derived state exists.
EventCachenotifies observers (refreshNewNoteObservers) as part of consuming an event, before some of what it derives from it (channels, indexes) is filled in. Re-read state in the collector rather than trusting it is complete. - Memory pressure trims.
MemoryTrimmingService(model/cache/) is run by both app modules, andAccountFeedContentStatestrims every feed to 200 items onmemoryPressure. Never assume a previously seen note is still resident.
References
references/account-state-flow.md— catalog ofAccountstate objects, kinds and packages.references/local-cache.md—EventCachestructures, insertion path, lookups, pruning.references/feeds.md— adding a feed: filter, state holder, invalidation, rendering, tests.- Related skills: quartz's
nostr-expert(event classes,EventFactory),relay-client(subscriptions),auth-signers(how mutations sign),compose-expert(rendering).