mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list budget (2% of the context window, or 8,000 chars when unknown), and a review found most bodies stale: paths from before the commons/commonsUI moves, the deleted legacy desktop app, a callback signer API that no longer exists, and example APIs that were never in the tree. Kept, rewritten and checked against the code (every path, class and command): account-state (+feeds), amy-expert, android-expert, auth-signers, compose-expert (+API style, Navigation 3 back stacks), desktop-expert (+run/package), event-store-semantics (+NIP-50 search), find-missing- translations, kotlin-coroutines (+structured concurrency, flow modeling), kotlin-multiplatform (+Gradle, composable extraction), ngit-pr, nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog), relay-client. Dropped: textbook Compose/Kotlin technique skills, kotlin-expert, kotlin-types-value-class, gradle-expert, quartz-integration (external-facing; see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands. The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md (stale history) and repoints SearchableEvent's KDoc to event-store-semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
7.2 KiB
Markdown
112 lines
7.2 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 `nostr-expert`).
|
|
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: `nostr-expert` (event classes, `EventFactory`), `relay-client` (subscriptions),
|
|
`auth-signers` (how mutations sign), `compose-expert` (rendering).
|