Follow-up to the event class renames: the names around those classes now
match too.
Quartz (published API, every old name kept as a @Deprecated alias/forwarder):
- nip51Lists packages moved: followList -> starterPack, hashtagList -> interestList,
peopleList -> followSet, labeledBookmarkList -> bookmarkSet. The old packages
keep a Deprecated.kt with typealiases and forwarding extension functions
(ReplaceWith points at the new package).
- LnZapPrivateEvent -> PrivateZapEvent, LnZapReceiptValidator -> ZapReceiptValidator,
ChannelListDiff -> PublicChatListDiff, HashtagListDiff -> InterestListDiff.
- Kind 30063 now indexes NIP-82 release notes for search (never NIP-51 content,
which can hold encrypted private items). searchable-events docs updated.
App code (amethyst, commons, desktopApp, cli; no aliases needed):
- model packages nip51Lists.{hashtagLists,peopleList,labeledBookmarkLists,relayFeeds}
-> {interestLists,followSets,bookmarkSets,favoriteRelays}.
- State/cache/UI names derived from the old event names: HashtagListState ->
InterestListState, PeopleListsState -> FollowSetsState, FollowListsState ->
StarterPacksState, LabeledBookmarkList -> BookmarkSet, RelayFeedListState ->
FavoriteRelayListState, ContactCardsState -> UserAssertionsState, NIP90* view
models/renderers -> Dvm*, LnZap* handlers -> Zap*, SealedRumor* -> Seal*, and
Account.peopleLists/followLists/hashtagList -> followSets/starterPacks/interestList.
- String literals were left untouched, so preference keys and @SerialName values
stored on disk are unchanged.
- The generic PeopleList UI model (shared by follow sets and starter packs) and the
EmojiPackSelection route (which shows a kind 30030 pack) keep their names.
Docs: plans, brainstorms, changelogs, skills and READMEs use the new names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD
11 KiB
Shared Metadata Loading — Upstream Reconciliation Plan
Redo the commons extraction of the per-user metadata finder and per-note event finder on top of current upstream/main, abandoning the stale rebase of worktree-plan-shared-metadata-loading (682c6000f5).
0. Executive summary of what changed under us
Upstream re-architected the same subsystem but also completed the model-sharing prerequisite we depended on:
amethyst.model.User,UserContext,Noteare now typealiases tocommons.model.*(amethyst/model/User.kt).SincePerRelayMap,MutableTime,EOSERelayListare typealiases tocommons.relays.*(amethyst/service/relays/EOSE.kt). Finder bodies are already type-compatible with commonMain.LocalCacheis anobjectimplementingcommons.model.cache.ICacheProvider(exposesgetUserIfExists,getNoteIfExists,getOrCreateUser,relayHints, …). The finders call exactly these.- Two new cross-cutting mechanisms:
AccountScopedQuery { val account: amethyst.model.Account }— implemented byUserFinderQueryState/EventFinderQueryStateso base managers can attribute subscriptions to an account.ExplainedFilter/SubPurpose(already in commons) — every emitted filter is tagged with its purpose.
Most important finding: the base commons managers never read .account. Attribution lives in amethyst-side managers (PerUserEoseManager, PerUniqueIdEoseManager, …) and each reads exactly (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex — only a pubkey hex. AND the four finder sub-assemblers extend the commons base managers, not the amethyst attribution managers — they attribute inline by computing soleAccountPubKey and passing accountPubKeys into their ExplainedFilters. So the finders do not depend on the amethyst attribution managers; they need only a pubkey.
A. Map of upstream's current subsystem
User side — amethyst/.../reqCommand/user/
UserFinderFilterAssembler.kt—UserFinderQueryState(user, override val account: Account) : AccountScopedQuery; groupsUserOutboxFinderSubAssembler,UserWatcherSubAssembler,UserReportsSubAssembler,UserCardsSubAssembler.UserFinderFilterAssemblerSubscription.kt— composable entry ((user, accountViewModel)) +UserFinderByParentFilterAssemblerSubscription; usesAccountViewModel.account,dataSources().userFinder, commonsLifecycleAwareKeyDataSourceSubscription.UserObservers.kt—observeUser*.loaders/UserOutboxFinderSubAssembler.kt— extends commonsBaseEoseManager; readsit.account→pickRelaysToLoadUsers(...); emitsExplainedFilter(purpose = RELAY_LISTS, accountPubKeys = listOfNotNull(soleAccountPubKey)).watchers/UserWatcherSubAssembler.kt— commonsBaseEoseManager; readsaccount.indexerRelayList.flow.value; callsfilterUserMetadataForKey(...).watchers/FilterUserMetadataForKey.kt— emitsExplainedFilter(PROFILE_METADATA, …); readsLocalCache.relayHints.hintsForKey(...)statically (the one non-injected cache ref).watchers/UserReportsSubAssembler.kt— commonsSingleSubEoseManager; readsaccount.declaredFollowsPerOutboxRelay.value,account.userProfile().pubkeyHex.watchers/UserCardsSubAssembler.kt— commonsSingleSubEoseManager; readsaccount.homeRelays.flow.value,account.trustProviderList.liveUserRankProvider,…liveUserFollowerCount,account.userProfile().pubkeyHex; emitsfilterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay(...)(already in commonsassemblers/ContactCardFilters.kt).
Event side — amethyst/.../reqCommand/event/
EventFinderFilterAssembler.kt—EventFinderQueryState(note, override val account: Account) : AccountScopedQuery; groupsNoteEventLoaderSubAssembler,EventWatcherSubAssembler,AddressableAuthorRelayLoaderSubAssembler(cache, ::allKeys, userFinder).EventFinderFilterAssemblerSubscription.kt— readsaccountViewModel.account,dataSources().eventFinder.EventObservers.kt—observeNote*(one UI-only spot readsaccountViewModel.account.userProfile().pubkeyHexfor a moderator check).loaders/NoteEventLoaderSubAssembler.kt;loaders/FilterMissingEvents.kt(readskey.account.followPlusAllMineWithSearch.flow.value,key.account.searchRelayList.flow.value;SubPurpose.REFERENCED_EVENTS);loaders/FilterMissingAddressables.kt(REFERENCED_EVENTS).loaders/AddressableAuthorRelayLoaderSubAssembler.kt— constructsUserFinderQueryState(author, key.account).watchers/EventWatcherSubAssembler.kt— commonsSingleSubEoseManager; readsit.account.userProfile().pubkeyHex(attribution only); callsfilterRepliesAndReactionsToNotes/Addresses(...).watchers/FilterRepliesAndReactionsToNotes.kt,FilterRepliesAndReactionsToAddresses.kt— emitExplainedFilter(ENGAGEMENT, …).
Base managers and attribution
- commons
BaseEoseManager,PerKeyEoseManager,SingleSubEoseManager— account-agnostic. - amethyst
PerUserEoseManager,PerUniqueIdEoseManager,PerUserAndFollowListEoseManager,SingleSubNoEoseCacheEoseManager— dof.attributedTo(pk)wherepk = (key as? AccountScopedQuery).account.userProfile().pubkeyHex. The finder sub-assemblers do NOT use these; they attribute inline. ExplainedFilter.attributedTo(accountPubKey: HexKey);SubPurpose/SubPurposeGroup— commonMain.
B. Account-seam decision — CHOSEN
Keep UserFinderAccount as the narrow commons seam, carrying the attribution pubkey via userFinderPubkeyHex. Do NOT move AccountScopedQuery to commons. Drop AccountScopedQuery from the two finder query states.
Justification (grounded in code):
- Attribution reduces to
.userProfile().pubkeyHex— aHexKey, already onUserFinderAccount.userFinderPubkeyHex. - The loaders' account reads are all snapshot relay-hint / follow-graph getters — exactly the
UserFinderAccountsurface (+2 additions). AccountScopedQueryis declared overamethyst.model.Accountand implemented by ~66 amethyst query states — can't move without draggingAccount.- The finder query states never flow through the amethyst attribution managers, so they don't need
AccountScopedQueryat all → dropping it removes the collision.
Seam shape (commons UserFinderAccount), = our old branch + 2 additions:
interface UserFinderAccount {
val userFinderPubkeyHex: HexKey // attribution pubkey → ExplainedFilter.accountPubKeys
fun indexRelays(): Set<NormalizedRelayUrl>
fun outboxHomeRelays(): Set<NormalizedRelayUrl>
fun searchRelays(): Set<NormalizedRelayUrl>
fun followPlusAllMineWithSearchRelays(): Set<NormalizedRelayUrl>
fun commonRelays(): Set<NormalizedRelayUrl>
fun cardHomeRelays(): Set<NormalizedRelayUrl>
fun trustProvider(): ServiceProviderTag?
fun followerCountProvider(): ServiceProviderTag? // NEW (UserCards reads liveUserFollowerCount)
fun declaredFollowsByOutboxRelay(): Map<NormalizedRelayUrl, Set<HexKey>>
}
EventFinderQueryState reuses UserFinderAccount (needs only follow+search relays + pubkey). Attribution: soleAccountPubKey = keys.map { it.account.userFinderPubkeyHex }.singleOrNull() → accountPubKeys = listOfNotNull(soleAccountPubKey).
C. SubPurpose mapping (preserve upstream tags across the move)
| Moved helper | SubPurpose |
|---|---|
FilterUserMetadataForKey (kind 0 bundle) |
PROFILE_METADATA (runsInBackground) |
UserOutboxFinderSubAssembler |
RELAY_LISTS |
UserCardsSubAssembler → ContactCardFilters (already commons) |
unchanged |
UserReportsSubAssembler → filterReportsToKeysFromTrusted |
MODERATION |
EventWatcherSubAssembler → replies/reactions |
ENGAGEMENT |
NoteEventLoader → FilterMissingEvents/Addressables |
REFERENCED_EVENTS |
ExplainedFilter/SubPurpose/attributedTo/ContactCardFilters are already commonMain + iOS-pure. Only change: accountPubKeys sourced from userFinderPubkeyHex.
D. Portable-unchanged vs must-rework
Unchanged (body identical after package move + seam swap): the filter builders (FilterUserMetadataForKey, FilterReportsToKey, FilterMissingEvents/Addressables, FilterRepliesAndReactionsToNotes/Addresses), observeUser*/observeNote*, UserFinderAccount + CompositionLocals, pickRelaysToLoadUsers inner overload.
Must rework:
- Move
EOSEAccountFast<T>(amethyst/service/relays/EOSE.kt) to commonsrelays/+ amethyst typealias — prereq for the loaders. (Purity risk: verify noSystem.currentTimeMillis/Thread.sleep.) LocalCache.relayHintsstatic ref inFilterUserMetadataForKey→ injectedcache.relayHints(addval relayHints: HintIndexertoICacheProvider; LocalCache already has it).- Account-seam swap across the four user sub-assemblers + event loaders/watchers (getter-for-flow mapping listed in §B).
pickRelaysToLoadUsers— feed commons inner overload fromUserFinderAccountgetters (amethyst keeps a thinAccount→relay-set wrapper).- Drop
AccountScopedQueryfrom the two finder query states (verified no amethyst attribution manager consumes them). AddressableAuthorRelayLoaderSubAssembler—UserFinderQueryState(author, key.account)now takesUserFinderAccount. Clean.
Desktop wiring (Phase 3/3b old plan) — unaffected: observeUser*, EventFinderFilterAssemblerSubscription(note), Locals, DM/search/notifications adoption, fast index-relay warm-up. DesktopIAccount adds followerCountProvider() = null (already degrades trust/declaredFollows).
E. Phase / commit sequence (fresh branch feat/shared-metadata-loading-v2 off upstream/main)
Cherry-pick content, not commits. Gates after each commons/amethyst commit: :commons:verifyKmpPurity, :amethyst:compilePlayDebugKotlin, :commons:compileKotlinJvm + :desktopApp:compileKotlinJvm.
- Phase 0 — prereqs: (0a) move
EOSEAccountFast→ commons + typealias; (0b) addrelayHintstoICacheProvider. - Phase 1 — seam: (1a) add commons
UserFinderAccount(+followerCountProvider()); (1b)Account implements UserFinderAccount. - Phase 2 — user finder → commons: (2a) filter builders +
pickRelaysToLoadUsersinner; (2b) the 4 sub-assemblers; (2c)UserFinderFilterAssembler+UserFinderQueryState(dropAccountScopedQuery) + amethyst typealias shim + commonsUserFinderSubscription/Locals; keep composable subscription in amethyst delegating. - Phase 3 — event finder → commons: (3a) filter builders; (3b) loaders/watchers + addressable bridge; (3c) assembler+state (drop
AccountScopedQuery) + shim +LocalEventFinder+observeNote*to commons. - Phase 4 — Desktop wiring: (4a)
DesktopIAccount implements UserFinderAccount; (4b) provide Locals inMain.kt, wire DM/search/notifications + warm-up ontoobserveUser*/EventFinderFilterAssemblerSubscription. - Phase 5 — cleanup & tests: delete dead originals; run
ExplainedFilterTest+ finder tests + full:commons:check.
Risk callouts
EOSEAccountFastpurity is the likeliestverifyKmpPuritytripwire.- Preserve inline
soleAccountPubKeyattribution so "Active Relay Subscriptions" still files single-account REQs correctly (and shows "not attributed" when accounts pool relays — don't regress to per-account splitting). - Do NOT re-introduce
AccountScopedQueryon the two finder states; if a future upstream manager consumes them, add a thin amethyst adapter instead of widening the commons seam.