mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
docs: upstream reconciliation plan for shared metadata loading (v2)
Redo the commons extraction of the per-user + per-note finders on top of current upstream/main, which re-architected the subsystem (AccountScopedQuery attribution + SubPurpose/ExplainedFilter tagging). Key finding: attribution needs only a pubkey, so the narrow UserFinderAccount seam survives and the finder query states can drop AccountScopedQuery entirely. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a5507f9a4d
commit
6e5f8c0fa0
@@ -0,0 +1,110 @@
|
||||
# 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`, `Note` are now **typealiases** to `commons.model.*` (`amethyst/model/User.kt`). `SincePerRelayMap`, `MutableTime`, `EOSERelayList` are typealiases to `commons.relays.*` (`amethyst/service/relays/EOSE.kt`). Finder bodies are already type-compatible with commonMain.
|
||||
- `LocalCache` is an `object` implementing `commons.model.cache.ICacheProvider` (exposes `getUserIfExists`, `getNoteIfExists`, `getOrCreateUser`, `relayHints`, …). The finders call exactly these.
|
||||
- **Two new cross-cutting mechanisms**:
|
||||
1. `AccountScopedQuery { val account: amethyst.model.Account }` — implemented by `UserFinderQueryState`/`EventFinderQueryState` so base managers can attribute subscriptions to an account.
|
||||
2. `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 `ExplainedFilter`s. 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`; groups `UserOutboxFinderSubAssembler`, `UserWatcherSubAssembler`, `UserReportsSubAssembler`, `UserCardsSubAssembler`.
|
||||
- `UserFinderFilterAssemblerSubscription.kt` — composable entry (`(user, accountViewModel)`) + `UserFinderByParentFilterAssemblerSubscription`; uses `AccountViewModel.account`, `dataSources().userFinder`, commons `LifecycleAwareKeyDataSourceSubscription`.
|
||||
- `UserObservers.kt` — `observeUser*`.
|
||||
- `loaders/UserOutboxFinderSubAssembler.kt` — extends commons `BaseEoseManager`; reads `it.account` → `pickRelaysToLoadUsers(...)`; emits `ExplainedFilter(purpose = RELAY_LISTS, accountPubKeys = listOfNotNull(soleAccountPubKey))`.
|
||||
- `watchers/UserWatcherSubAssembler.kt` — commons `BaseEoseManager`; reads `account.indexerRelayList.flow.value`; calls `filterUserMetadataForKey(...)`.
|
||||
- `watchers/FilterUserMetadataForKey.kt` — emits `ExplainedFilter(PROFILE_METADATA, …)`; reads `LocalCache.relayHints.hintsForKey(...)` **statically** (the one non-injected cache ref).
|
||||
- `watchers/UserReportsSubAssembler.kt` — commons `SingleSubEoseManager`; reads `account.declaredFollowsPerOutboxRelay.value`, `account.userProfile().pubkeyHex`.
|
||||
- `watchers/UserCardsSubAssembler.kt` — commons `SingleSubEoseManager`; reads `account.homeRelays.flow.value`, `account.trustProviderList.liveUserRankProvider`, `…liveUserFollowerCount`, `account.userProfile().pubkeyHex`; emits `filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(...)` (already in commons `assemblers/ContactCardFilters.kt`).
|
||||
|
||||
### Event side — `amethyst/.../reqCommand/event/`
|
||||
- `EventFinderFilterAssembler.kt` — `EventFinderQueryState(note, override val account: Account) : AccountScopedQuery`; groups `NoteEventLoaderSubAssembler`, `EventWatcherSubAssembler`, `AddressableAuthorRelayLoaderSubAssembler(cache, ::allKeys, userFinder)`.
|
||||
- `EventFinderFilterAssemblerSubscription.kt` — reads `accountViewModel.account`, `dataSources().eventFinder`.
|
||||
- `EventObservers.kt` — `observeNote*` (one UI-only spot reads `accountViewModel.account.userProfile().pubkeyHex` for a moderator check).
|
||||
- `loaders/NoteEventLoaderSubAssembler.kt`; `loaders/FilterMissingEvents.kt` (reads `key.account.followPlusAllMineWithSearch.flow.value`, `key.account.searchRelayList.flow.value`; `SubPurpose.REFERENCED_EVENTS`); `loaders/FilterMissingAddressables.kt` (`REFERENCED_EVENTS`).
|
||||
- `loaders/AddressableAuthorRelayLoaderSubAssembler.kt` — constructs `UserFinderQueryState(author, key.account)`.
|
||||
- `watchers/EventWatcherSubAssembler.kt` — commons `SingleSubEoseManager`; reads `it.account.userProfile().pubkeyHex` (attribution only); calls `filterRepliesAndReactionsToNotes/Addresses(...)`.
|
||||
- `watchers/FilterRepliesAndReactionsToNotes.kt`, `FilterRepliesAndReactionsToAddresses.kt` — emit `ExplainedFilter(ENGAGEMENT, …)`.
|
||||
|
||||
### Base managers and attribution
|
||||
- commons `BaseEoseManager`, `PerKeyEoseManager`, `SingleSubEoseManager` — **account-agnostic**.
|
||||
- amethyst `PerUserEoseManager`, `PerUniqueIdEoseManager`, `PerUserAndFollowListEoseManager`, `SingleSubNoEoseCacheEoseManager` — do `f.attributedTo(pk)` where `pk = (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):
|
||||
1. Attribution reduces to `.userProfile().pubkeyHex` — a `HexKey`, already on `UserFinderAccount.userFinderPubkeyHex`.
|
||||
2. The loaders' account reads are all snapshot relay-hint / follow-graph getters — exactly the `UserFinderAccount` surface (+2 additions).
|
||||
3. `AccountScopedQuery` is declared over `amethyst.model.Account` and implemented by ~66 amethyst query states — can't move without dragging `Account`.
|
||||
4. The finder query states never flow through the amethyst attribution managers, so they don't need `AccountScopedQuery` at all → dropping it removes the collision.
|
||||
|
||||
Seam shape (commons `UserFinderAccount`), = our old branch + 2 additions:
|
||||
```kotlin
|
||||
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:**
|
||||
1. **Move `EOSEAccountFast<T>`** (`amethyst/service/relays/EOSE.kt`) to commons `relays/` + amethyst typealias — prereq for the loaders. (Purity risk: verify no `System.currentTimeMillis`/`Thread.sleep`.)
|
||||
2. **`LocalCache.relayHints` static ref** in `FilterUserMetadataForKey` → injected `cache.relayHints` (add `val relayHints: HintIndexer` to `ICacheProvider`; LocalCache already has it).
|
||||
3. **Account-seam swap** across the four user sub-assemblers + event loaders/watchers (getter-for-flow mapping listed in §B).
|
||||
4. **`pickRelaysToLoadUsers`** — feed commons inner overload from `UserFinderAccount` getters (amethyst keeps a thin `Account`→relay-set wrapper).
|
||||
5. **Drop `AccountScopedQuery`** from the two finder query states (verified no amethyst attribution manager consumes them).
|
||||
6. `AddressableAuthorRelayLoaderSubAssembler` — `UserFinderQueryState(author, key.account)` now takes `UserFinderAccount`. 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) add `relayHints` to `ICacheProvider`.
|
||||
- **Phase 1 — seam:** (1a) add commons `UserFinderAccount` (+`followerCountProvider()`); (1b) `Account implements UserFinderAccount`.
|
||||
- **Phase 2 — user finder → commons:** (2a) filter builders + `pickRelaysToLoadUsers` inner; (2b) the 4 sub-assemblers; (2c) `UserFinderFilterAssembler`+`UserFinderQueryState` (drop `AccountScopedQuery`) + amethyst typealias shim + commons `UserFinderSubscription`/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 in `Main.kt`, wire DM/search/notifications + warm-up onto `observeUser*`/`EventFinderFilterAssemblerSubscription`.
|
||||
- **Phase 5 — cleanup & tests:** delete dead originals; run `ExplainedFilterTest` + finder tests + full `:commons:check`.
|
||||
|
||||
### Risk callouts
|
||||
- `EOSEAccountFast` purity is the likeliest `verifyKmpPurity` tripwire.
|
||||
- Preserve inline `soleAccountPubKey` attribution 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 `AccountScopedQuery` on the two finder states; if a future upstream manager consumes them, add a thin amethyst adapter instead of widening the commons seam.
|
||||
Reference in New Issue
Block a user