Merge pull request #3019 from vitorpamplona/claude/debug-metadata-relay-loading-opoto

Add fallback relay logic for abandoned users in metadata queries
This commit is contained in:
Vitor Pamplona
2026-05-20 16:52:26 -04:00
committed by GitHub
6 changed files with 66 additions and 14 deletions
@@ -404,7 +404,7 @@ class Account(
val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope)
val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope)
val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, broadcastRelayList, scope)
val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, indexerRelayList, proxyRelayList, trustedRelayList, broadcastRelayList, scope)
// Follows Relays
val followOutboxesOrProxy = FollowListOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope)
@@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
@@ -43,6 +45,8 @@ class TrustedRelayListsState(
val localRelayList: LocalRelayListState,
val dmRelayList: DmRelayListState,
val searchRelayListState: SearchRelayListState,
val indexerRelayList: IndexerRelayListState,
val proxyRelayList: ProxyRelayListState,
val trustedRelayList: TrustedRelayListState,
val broadcastRelayList: BroadcastRelayListState,
val scope: CoroutineScope,
@@ -57,6 +61,8 @@ class TrustedRelayListsState(
localRelayList.flow,
dmRelayList.flow,
searchRelayListState.flowNoDefaults,
indexerRelayList.flowNoDefaults,
proxyRelayList.flow,
trustedRelayList.flow,
broadcastRelayList.flow,
),
@@ -70,6 +76,8 @@ class TrustedRelayListsState(
localRelayList.flow.value,
dmRelayList.flow.value,
searchRelayListState.flowNoDefaults.value,
indexerRelayList.flowNoDefaults.value,
proxyRelayList.flow.value,
trustedRelayList.flow.value,
broadcastRelayList.flow.value,
),
@@ -86,6 +94,8 @@ class TrustedRelayListsState(
localRelayList.flow.value,
dmRelayList.flow.value,
searchRelayListState.flowNoDefaults.value,
indexerRelayList.flowNoDefaults.value,
proxyRelayList.flow.value,
trustedRelayList.flow.value,
broadcastRelayList.flow.value,
),
@@ -48,7 +48,7 @@ class UserFinderFilterAssembler(
val group =
listOf(
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
UserWatcherSubAssembler(client, cache, ::allKeys),
UserWatcherSubAssembler(client, cache, failureTracker, ::allKeys),
UserReportsSubAssembler(client, cache, ::allKeys),
UserCardsSubAssembler(client, cache, ::allKeys),
)
@@ -20,12 +20,15 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders
import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
@@ -107,16 +110,46 @@ class UserOutboxFinderSubAssembler(
hasTried,
)
return perRelayKeysBoth.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = relayListKinds, authors = sortedUsers),
)
} else {
null
val activeFilters =
perRelayKeysBoth.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = relayListKinds, authors = sortedUsers),
)
} else {
null
}
}
}
// Users that pickRelaysToLoadUsers could not place anywhere this pass:
// every candidate relay tier (outbox/hints/index/search/connected/common)
// is already in hasTried. Without a fallback they'd be silently dropped
// and a late-published kind 10002 would never reach the UI.
val placedPubkeys =
perRelayKeysBoth.values
.asSequence()
.flatten()
.toSet()
val abandonedPubkeys =
noOutboxList.mapNotNullTo(mutableSetOf<HexKey>()) { user ->
user.pubkeyHex.takeIf { it !in placedPubkeys }
}
if (abandonedPubkeys.isEmpty()) return activeFilters
val sortedAbandoned = abandonedPubkeys.sorted()
val fallbackRelays =
(DefaultIndexerRelayList + DefaultSearchRelayList) - failureTracker.cannotConnectRelays
val fallbackFilters =
fallbackRelays.map { relay ->
RelayBasedFilter(
relay = relay,
filter = Filter(kinds = relayListKinds, authors = sortedAbandoned),
)
}
return activeFilters + fallbackFilters
}
}
@@ -50,6 +50,7 @@ val UserMetadataForKeyKinds =
fun filterUserMetadataForKey(
authors: Set<User>,
indexRelays: Set<NormalizedRelayUrl>,
cannotConnectRelays: Set<NormalizedRelayUrl>,
since: EOSEAccountFast<User>,
): List<RelayBasedFilter> {
val perRelayUsers =
@@ -59,7 +60,7 @@ fun filterUserMetadataForKey(
key.outboxRelays()
?: (key.allUsedRelays() + LocalCache.relayHints.hintsForKey(key.pubkeyHex) + indexRelays)
relays.forEach {
(relays - cannotConnectRelays).forEach {
add(it, key)
}
}
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -37,6 +38,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
class UserWatcherSubAssembler(
client: INostrClient,
val cache: LocalCache,
val failureTracker: RelayOfflineTracker,
allKeys: () -> Set<UserFinderQueryState>,
) : BaseEoseManager<UserFinderQueryState>(client, allKeys) {
/**
@@ -99,7 +101,13 @@ class UserWatcherSubAssembler(
)
}
val newFilters = filterUserMetadataForKey(users, indexRelays, latestEOSEs).ifEmpty { null }
val newFilters =
filterUserMetadataForKey(
users,
indexRelays,
failureTracker.cannotConnectRelays,
latestEOSEs,
).ifEmpty { null }
sub.updateFilters(newFilters?.groupByRelay())
}