fix: keep listening on default index+search relays for users with no kind 10002

When pickRelaysToLoadUsers exhausts every candidate tier (outbox / hints /
index / search / connected / common) for a user without finding kind 10002,
it returns an empty map and the REQ closes. A late-published relay list
from that user would never reach the UI for the rest of the session
(unless EOSEAccountFast's LRU evicts the entry).

Compute the set of users we couldn't place this pass and add a permanent
fallback filter on DefaultIndexerRelayList + DefaultSearchRelayList. The
filter is deterministic (sorted authors, set-based relays) so the relay
client de-duplicates it across passes, keeping the REQ open without
re-sending — any future kind 0 / kind 10002 publication will be pushed
to us live.

Offline relays are subtracted via failureTracker.cannotConnectRelays.
This commit is contained in:
Claude
2026-05-20 19:57:38 +00:00
parent 5962f7d659
commit 13a599e282
@@ -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
}
}