mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
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
113 lines
7.3 KiB
Markdown
113 lines
7.3 KiB
Markdown
---
|
|
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()`. Every `Note` / `User` the client has
|
|
seen. All logic is in `model/cache/EventCache.kt` (an open class so tests can build their own);
|
|
Android and Desktop both use the `LocalCache` singleton (`AppModules.kt`, `DesktopAppModules.kt`).
|
|
- `model/Account.kt` — one `Account` per signed-in user: ~50 per-feature **state objects**
|
|
(`kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, …), each exposing `.flow`.
|
|
- `feeds/` + `viewmodels/` + `<feature>/dal/` — feeds: a `FeedFilter` scans `LocalCache`, a
|
|
`FeedContentState` / `FeedViewModel` keeps 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`'s `notes` / `addressables` are `LargeSoftCache` (weak references), so the state
|
|
object's field is what keeps its note from being collected.
|
|
- **Expose one `StateFlow`.** Map `muteListNote.flow().metadata.stateFlow` → decrypt (private lists go
|
|
through a per-feature `…DecryptionCache` such as `MuteListDecryptionCache`, held next to it in
|
|
`Account` as `muteListDecryptionCache`) → fall back to the `AccountSettings` backup
|
|
(`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 a `MuteListEvent`; `Account.hideUser` does
|
|
`sendMyPublicAndPrivateOutbox(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 a `MutableStateFlow` on `Account`.
|
|
|
|
Full catalog of properties, classes, kinds and packages: `references/account-state-flow.md`.
|
|
|
|
### Adding an account-scoped setting or list
|
|
|
|
1. 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-expert` skill; a new one is a quartz
|
|
change, built here with `-PquartzDir=../quartz`).
|
|
2. Add `commons/…/model/nipXX…/XState.kt` modeled on `MuteListState` (encrypted) or
|
|
`nip51Lists/BookmarkListState.kt` (plain): pin, flow, suspend mutation helpers that build the
|
|
event with the quartz class and sign it.
|
|
3. Instantiate it as a `val` in `Account.kt` (plus its `DecryptionCache` if private) and add an
|
|
`Account` method that sends the returned event (`sendMyPublicAndPrivateOutbox`, `sendAutomatic`, …).
|
|
If the list must survive relay loss, add a `backupX` field to `AccountSettings`.
|
|
4. Make sure `EventCache.justConsumeInnerInner` lists the event class — a kind that is not listed
|
|
falls into the `else` branch and is **rejected**, so the state object never sees it, even though
|
|
`EventFactory` parses it. Replaceable kinds without custom logic go in the
|
|
`consumeBaseReplaceable` group.
|
|
5. Relay subscription: `relay-client`. UI: settings screens are shared in
|
|
`commonsUI/…/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 `FeedContentState`s in `viewmodels/AccountFeedContentStates.kt`
|
|
(owned by `BaseAccountViewModel.feedStates`); per-screen feeds use a `FeedViewModel` subclass. Recipe,
|
|
real code and traps: `references/feeds.md`.
|
|
|
|
## Gotchas
|
|
|
|
- **`LocalCache` is shared across accounts.** Switching accounts doesn't wipe it; every `Account`
|
|
derives its flows from the same cache. Filters that depend on the account must put the account
|
|
pubkey in `feedKey()`.
|
|
- **Hold the `Note`, not the `Event`.** `getOrCreate*` returns the same wrapper per id; for
|
|
replaceable/addressable notes `note.event` is swapped in place when a newer version arrives.
|
|
- **Flows on a `Note` are not durable.** `CachePruner` (`model/cache/CachePruner.kt`) calls
|
|
`clearFlow()` on notes during cleanup, and anything not strongly held can be collected. Re-fetch
|
|
via `note.flow()`; don't stash flow references.
|
|
- **Observers can fire before derived state exists.** `EventCache` notifies 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,
|
|
and `AccountFeedContentStates` trims every feed to 200 items on `memoryPressure`. Never assume a
|
|
previously seen note is still resident.
|
|
|
|
## References
|
|
|
|
- `references/account-state-flow.md` — catalog of `Account` state objects, kinds and packages.
|
|
- `references/local-cache.md` — `EventCache` structures, 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).
|