diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index db90fd8b7c..7c1c753072 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -20,66 +20,96 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +/** + * Each account's own profile, lists and recent posts — for **every** logged-in account, in one + * subscription. + * + * Every filter here is `authors`-keyed, so a relay that several accounts read from can be asked + * about all of them at once by widening `authors` rather than opening a REQ per account. This was + * the largest single contributor to blowing a relay's `max_subscriptions`: seven filters in a + * subscription, repeated once per account. + * + * The per-account `limit`s are summed rather than shared. These are mostly replaceable events, so + * the limit is a safety bound rather than a page size, and scaling it by the number of accounts + * keeps each one exactly the headroom it had alone. + */ class AccountMetadataEoseManager( client: INostrClient, allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun user(key: AccountQueryState) = key.account.userProfile() +) : SingleSubEoseManager(client, allKeys) { + override fun distinct(key: AccountQueryState) = key.account.userProfile() fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow override fun updateFilter( - key: AccountQueryState, + keys: List, since: SincePerRelayMap?, - ): List = - relayFlow(key).value.flatMap { - val since = since?.get(it)?.time - listOf( - filterAccountInfoAndListsFromKey(it, user(key).pubkeyHex, since), - filterFollowsAndMutesFromKey(it, user(key).pubkeyHex, since), - filterBookmarksAndReportsFromKey(it, user(key).pubkeyHex, since), - filterLastPostsFromKey(it, user(key).pubkeyHex, since ?: TimeUtils.oneMonthAgo()), - filterBasicAccountInfoFromKeys(it, key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since), - ).flatten() + ): List { + val accountsPerRelay = mutableMapOf>() + keys.forEach { key -> + relayFlow(key).value.forEach { relay -> + accountsPerRelay.getOrPut(relay) { mutableListOf() }.add(key) + } } - val userJobMap = mutableMapOf>() + return accountsPerRelay.flatMap { (relay, accounts) -> + val pubkeys = accounts.map { it.account.userProfile().pubkeyHex } + val relaySince = since?.get(relay)?.time + + // The account-switcher avatars: other logged-in accounts this screen wants to name. + // Screens supply them; the background registry does not, so this is usually empty. + val otherAccounts = accounts.flatMapTo(mutableSetOf()) { it.otherAccounts }.minus(pubkeys.toSet()) - @OptIn(FlowPreview::class) - override fun newSub(key: AccountQueryState): Subscription { - val user = user(key) - userJobMap[user]?.forEach { it.cancel() } - userJobMap[user] = listOf( - key.account.scope.launch(Dispatchers.IO) { - relayFlow(key).collectLatest { - invalidateFilters() - } - }, - ) - - return super.newSub(key) + filterAccountInfoAndListsFromKey(relay, pubkeys, relaySince), + filterFollowsAndMutesFromKey(relay, pubkeys, relaySince), + filterBookmarksAndReportsFromKey(relay, pubkeys, relaySince), + filterLastPostsFromKey(relay, pubkeys, relaySince ?: TimeUtils.oneMonthAgo()), + filterBasicAccountInfoFromKeys(relay, otherAccounts.toList(), relaySince), + ).flatten() + } } - override fun endSub( - key: User, - subId: String, - ) { - super.endSub(key, subId) - userJobMap[key]?.forEach { it.cancel() } + /** Per-account relay watchers, reconciled as accounts come and go. See the notifications manager. */ + private val userJobMap = mutableMapOf>() + + override fun updateSubscriptions(keys: Set) { + val wanted = keys.associateBy { it.account.userProfile() } + + (userJobMap.keys - wanted.keys).toList().forEach { user -> + userJobMap.remove(user)?.forEach { it.cancel() } + } + + wanted.forEach { (user, key) -> + if (user !in userJobMap) { + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.IO) { + relayFlow(key).collectLatest { invalidateFilters() } + }, + ) + } + } + + super.updateSubscriptions(keys) + } + + override fun destroy() { + userJobMap.values.forEach { jobs -> jobs.forEach { it.cancel() } } + userJobMap.clear() + super.destroy() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 35d9288085..9b9fbf6020 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -109,10 +109,10 @@ val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG)) fun filterAccountInfoAndListsFromKey( relay: NormalizedRelayUrl, - pubkey: HexKey, + pubkeys: List, since: Long?, ): List { - if (pubkey.isEmpty()) return emptyList() + if (pubkeys.isEmpty()) return emptyList() return listOf( RelayBasedFilter( @@ -121,8 +121,8 @@ fun filterAccountInfoAndListsFromKey( ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, kinds = AccountInfoAndListsFromKeyKinds, - authors = listOf(pubkey), - limit = 20, + authors = pubkeys, + limit = 20 * pubkeys.size, since = since, ), ), @@ -132,8 +132,8 @@ fun filterAccountInfoAndListsFromKey( ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, kinds = AccountInfoAndListsFromKeyKinds2, - authors = listOf(pubkey), - limit = 80, + authors = pubkeys, + limit = 80 * pubkeys.size, since = since, ), ), @@ -141,7 +141,7 @@ fun filterAccountInfoAndListsFromKey( // Addressable — one card per target user — hence its own larger-limit filter. filterContactCardsByAuthorInTheRelay( relay = relay, - author = pubkey, + authors = pubkeys, since = since, ), RelayBasedFilter( @@ -150,9 +150,9 @@ fun filterAccountInfoAndListsFromKey( ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, kinds = AmethystMetadataKinds, - authors = listOf(pubkey), + authors = pubkeys, tags = AmethystMetadataTagMapFilter, - limit = 1, + limit = 1 * pubkeys.size, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBookmarksAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBookmarksAndReportsFromKey.kt index edde979296..4b3af157bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBookmarksAndReportsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBookmarksAndReportsFromKey.kt @@ -44,10 +44,10 @@ val ReportsAndBookmarksFromKeyKinds = fun filterBookmarksAndReportsFromKey( relay: NormalizedRelayUrl, - pubkey: HexKey?, + pubkeys: List, since: Long?, ): List { - if (pubkey.isNullOrEmpty()) return emptyList() + if (pubkeys.isEmpty()) return emptyList() return listOf( RelayBasedFilter( @@ -56,7 +56,7 @@ fun filterBookmarksAndReportsFromKey( ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, kinds = ReportsAndBookmarksFromKeyKinds, - authors = listOf(pubkey), + authors = pubkeys, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt index ce53c3f448..c4a4bc0541 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -48,10 +48,10 @@ val FollowAndMutesFromKeyKinds = fun filterFollowsAndMutesFromKey( relay: NormalizedRelayUrl, - pubkey: HexKey, + pubkeys: List, since: Long?, ): List { - if (pubkey.isEmpty()) return emptyList() + if (pubkeys.isEmpty()) return emptyList() return listOf( RelayBasedFilter( @@ -60,8 +60,8 @@ fun filterFollowsAndMutesFromKey( ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, kinds = FollowAndMutesFromKeyKinds, - authors = listOf(pubkey), - limit = 100, + authors = pubkeys, + limit = 100 * pubkeys.size, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt index e0a534ae4e..6ec9021e2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt @@ -28,10 +28,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl fun filterLastPostsFromKey( relay: NormalizedRelayUrl, - pubkey: HexKey, + pubkeys: List, since: Long?, ): List { - if (pubkey.isEmpty()) return emptyList() + if (pubkeys.isEmpty()) return emptyList() return listOf( RelayBasedFilter( @@ -39,8 +39,8 @@ fun filterLastPostsFromKey( filter = ExplainedFilter( purpose = SubPurpose.ACCOUNT_DATA, - authors = listOf(pubkey), - limit = 100, + authors = pubkeys, + limit = 100 * pubkeys.size, since = since, ), ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt index b7bfc89949..f8d8c3a973 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt @@ -65,7 +65,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( */ fun filterContactCardsByAuthorInTheRelay( relay: NormalizedRelayUrl, - author: HexKey, + authors: List, since: Long?, limit: Int = 500, ): RelayBasedFilter = @@ -75,9 +75,9 @@ fun filterContactCardsByAuthorInTheRelay( ExplainedFilter( purpose = SubPurpose.PROFILE_METADATA, // This variant fetches an account's OWN contact card, so the author is the owner. - accountPubKeys = listOfNotNull(author), + accountPubKeys = authors, kinds = ContactCardKindList, - authors = listOf(author), + authors = authors, limit = limit, since = since, ),