From a36d35912a1339abe57114af6a23fbd82cb5b1c1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Nov 2025 17:12:39 -0500 Subject: [PATCH] Manages the pool state without having to loop through relays, saving some milliseconds of processing. --- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../loaders/UserOutboxFinderSubAssembler.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 103 ++++++++---------- .../connected/ConnectedRelayListViewModel.kt | 4 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 2 + .../nip01Core/relay/client/INostrClient.kt | 9 +- .../nip01Core/relay/client/NostrClient.kt | 4 +- .../nip01Core/relay/client/pool/RelayPool.kt | 80 +++++--------- 9 files changed, 90 insertions(+), 120 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0eb7fa0b5..e25bc0e08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -704,7 +704,7 @@ class Account( fun computeRelayListToBroadcast(event: Event): Set { if (event is MetadataEvent || event is AdvertisedRelayListEvent) { // everywhere - return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available + return followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value } if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() @@ -904,7 +904,7 @@ class Account( } fun sendLiterallyEverywhere(event: Event) { - client.send(event, followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available) + client.send(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value) cache.justConsumeMyOwnEvent(event) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 87c91c143..9c7db9910 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -154,7 +154,7 @@ class AccountFollowsLoaderSubAssembler( println("AccountFollowNeeds ${users.size}") - val connectedRelays = client.relayStatusFlow().value.connected + val connectedRelays = client.connectedRelaysFlow().value val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index bb145322d..76108f540 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -96,7 +96,7 @@ class UserOutboxFinderSubAssembler( if (noOutboxList.isEmpty()) return null val accounts = keys.mapTo(mutableSetOf()) { it.account } - val connectedRelays = client.relayStatusFlow().value.connected + val connectedRelays = client.connectedRelaysFlow().value val perRelayKeysBoth = pickRelaysToLoadUsers( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index a8604e363..2456f9d05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -112,11 +112,11 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Font18SP import com.vitorpamplona.amethyst.ui.theme.IconRowModifier import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier -import com.vitorpamplona.amethyst.ui.theme.Size16dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Width16Space import com.vitorpamplona.amethyst.ui.theme.bannerModifier import com.vitorpamplona.amethyst.ui.theme.drawerSpacing import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -124,7 +124,8 @@ import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine @Composable fun DrawerContent( @@ -537,40 +538,6 @@ fun ListContent( } } -@Composable -private fun RelayStatus(accountViewModel: AccountViewModel) { - val connectedRelaysText by accountViewModel.account.client - .relayStatusFlow() - .collectAsStateWithLifecycle() - - RenderRelayStatus(connectedRelaysText) -} - -@Composable -private fun RenderRelayStatus(relayPool: RelayPool.RelayPoolStatus) { - val text by - remember(relayPool) { derivedStateOf { "${relayPool.connected.size}/${relayPool.available.size}" } } - - val placeHolder = MaterialTheme.colorScheme.placeholderText - - val color by - remember(relayPool) { - derivedStateOf { - if (relayPool.isConnected) { - placeHolder - } else { - Color.Red - } - } - } - - Text( - text = text, - color = color, - style = MaterialTheme.typography.titleMedium, - ) -} - @Composable fun NavigationRow( title: Int, @@ -691,35 +658,55 @@ fun IconRowRelays( modifier = Modifier .fillMaxWidth() - .clickable { onClick() }, + .padding(vertical = 15.dp, horizontal = 25.dp) + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 15.dp, horizontal = 25.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterRes(R.drawable.relays, 4), - contentDescription = stringRes(R.string.relay_setup), - modifier = Modifier.size(22.dp), - tint = MaterialTheme.colorScheme.onSurface, - ) + Icon( + painter = painterRes(R.drawable.relays, 4), + contentDescription = stringRes(R.string.relay_setup), + modifier = Size22Modifier, + tint = MaterialTheme.colorScheme.onSurface, + ) - Text( - modifier = Modifier.padding(start = 16.dp), - text = stringRes(id = R.string.relay_setup), - fontSize = 18.sp, - ) + Text( + modifier = IconRowTextModifier, + text = stringRes(id = R.string.relay_setup), + fontSize = Font18SP, + ) - Spacer(modifier = Modifier.width(Size16dp)) + Spacer(modifier = Width16Space) - RelayStatus(accountViewModel = accountViewModel) - } + RelayStatus(accountViewModel) } } +class PoolStatus( + val share: String, + val isConnected: Boolean, +) + +@Composable +private fun RelayStatus(accountViewModel: AccountViewModel) { + val statusCounterFlow: Flow = + remember(accountViewModel) { + combine( + accountViewModel.account.client.connectedRelaysFlow(), + accountViewModel.account.client.availableRelaysFlow(), + ) { connected, available -> + PoolStatus("${connected.size}/${available.size}", connected.isNotEmpty()) + } + } + + val relayPool by statusCounterFlow.collectAsStateWithLifecycle(PoolStatus("", false)) + + Text( + text = relayPool.share, + color = if (relayPool.isConnected) MaterialTheme.colorScheme.placeholderText else Color.Red, + style = MaterialTheme.typography.titleMedium, + ) +} + @Composable fun BottomContent( user: User, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index 0479378fb..2c7463e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -50,8 +50,8 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { override fun getRelayList(): List = account.client - .relayStatusFlow() - .value.available + .availableRelaysFlow() + .value .sorted() override suspend fun saveRelayList(urlList: List) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index c322daa36..1fcd3daab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -255,6 +255,8 @@ val drawerSpacing = Modifier.padding(top = Size10dp, start = Size25dp, end = Siz val IconRowTextModifier = Modifier.padding(start = 16.dp) val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp) +val Width16Space = Modifier.width(Size16dp) + val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth() val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt index 41ac7b89a..2206257c1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.client import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId @@ -32,7 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow interface INostrClient { - fun relayStatusFlow(): StateFlow + fun connectedRelaysFlow(): StateFlow> + + fun availableRelaysFlow(): StateFlow> fun connect() @@ -80,7 +81,9 @@ interface INostrClient { } object EmptyNostrClient : INostrClient { - override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus()) + override fun connectedRelaysFlow() = MutableStateFlow(emptySet()) + + override fun availableRelaysFlow() = MutableStateFlow(emptySet()) override fun connect() { } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index c62e09d21..cc3ef2db3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -320,5 +320,7 @@ class NostrClient( override fun getCountFiltersOrNull(subId: String): Map>? = activeCounts.getSubscriptionFiltersOrNull(subId) - override fun relayStatusFlow() = relayPool.statusFlow + override fun connectedRelaysFlow() = relayPool.connectedRelays + + override fun availableRelaysFlow() = relayPool.availableRelays } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 40f6b9c4d..cf0490924 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.client.pool -import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -31,8 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update /** * RelayPool manages a collection of Nostr relays, abstracting individual connections and providing @@ -61,9 +60,11 @@ class RelayPool( ) : IRelayClientListener { private val relays = LargeCache() - // Backing property to avoid flow emissions from other classes - private val _statusFlow = MutableStateFlow(RelayPoolStatus()) - val statusFlow: StateFlow = _statusFlow.asStateFlow() + private val _connectedRelays = MutableStateFlow>(emptySet()) + val connectedRelays = _connectedRelays.asStateFlow() + + private val _availableRelays = MutableStateFlow>(emptySet()) + val availableRelays = _availableRelays.asStateFlow() fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url) @@ -87,21 +88,18 @@ class RelayPool( relay.connectAndSyncFiltersIfDisconnected(ignoreRetryDelays) } } - updateStatus() } fun connect() { relays.forEach { url, relay -> relay.connect() } - updateStatus() } fun connectIfDisconnected() { relays.forEach { url, relay -> relay.connectAndSyncFiltersIfDisconnected() } - updateStatus() } fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected() @@ -110,7 +108,6 @@ class RelayPool( relays.forEach { url, relay -> relay.disconnect() } - updateStatus() } fun sendOrConnectAndSync( @@ -144,7 +141,13 @@ class RelayPool( // -------------------- // Pool Maintenance // -------------------- - fun getOrCreateRelay(relay: NormalizedRelayUrl) = relays.getOrCreate(relay, ::createNewRelay) + fun getOrCreateRelay(relay: NormalizedRelayUrl): IRelayClient { + val r = relays.getOrCreate(relay, ::createNewRelay) + if (_availableRelays.value.size != relays.size()) { + _availableRelays.update { relays.keys() } + } + return r + } fun createRelayIfAbsent(relay: NormalizedRelayUrl): Boolean = relays.createIfAbsent(relay, ::createNewRelay) @@ -155,39 +158,40 @@ class RelayPool( val toRemove = relays.keys() - newRelays var atLeastOne = false - newRelays.forEach { - if (createRelayIfAbsent(it)) { + newRelays.forEach { relay -> + if (createRelayIfAbsent(relay)) { atLeastOne = true } } - toRemove.forEach { - if (removeRelayInner(it)) { + toRemove.forEach { relay -> + if (removeRelayInner(relay)) { atLeastOne = true } } if (atLeastOne) { - updateStatus() + _availableRelays.update { relays.keys() } } } fun addRelay(relay: NormalizedRelayUrl): IRelayClient { if (createRelayIfAbsent(relay)) { - updateStatus() + _availableRelays.update { relays.keys() } } return getOrCreateRelay(relay) } fun addAllRelays(relayList: List) { var atLeastOne = false - relayList.forEach { - if (createRelayIfAbsent(it)) { + relayList.forEach { relay -> + if (createRelayIfAbsent(relay)) { atLeastOne = true } } + if (atLeastOne) { - updateStatus() + _availableRelays.update { relays.keys() } } } @@ -202,7 +206,7 @@ class RelayPool( fun removeRelay(relay: NormalizedRelayUrl) { if (removeRelayInner(relay)) { - updateStatus() + _availableRelays.update { relays.keys() } } } @@ -210,7 +214,7 @@ class RelayPool( if (relays.size() > 0) { disconnect() relays.clear() - updateStatus() + _availableRelays.update { emptySet() } } } @@ -224,12 +228,12 @@ class RelayPool( pingMillis: Int, compressed: Boolean, ) { - updateStatus() + _connectedRelays.update { it + relay.url } listener.onConnected(relay, pingMillis, compressed) } override fun onDisconnected(relay: IRelayClient) { - updateStatus() + _connectedRelays.update { it - relay.url } listener.onDisconnected(relay) } @@ -251,33 +255,5 @@ class RelayPool( success: Boolean, ) = listener.onSent(relay, cmdStr, cmd, success) - // --------------- - // STATUS Reports - // --------------- - - fun availableRelays(): Set = relays.keys() - - fun connectedRelays(): Set = - relays.mapNotNullIntoSet { url, relay -> - if (relay.isConnected()) { - url - } else { - null - } - } - - private fun updateStatus() { - val connected = connectedRelays() - val available = availableRelays() - if (_statusFlow.value.connected != connected || _statusFlow.value.available != available) { - _statusFlow.tryEmit(RelayPoolStatus(connected, available)) - } - } - - @Immutable - data class RelayPoolStatus( - val connected: Set = emptySet(), - val available: Set = emptySet(), - val isConnected: Boolean = connected.isNotEmpty(), - ) + fun connectedRelaysCount(): Int = relays.count { url, relay -> relay.isConnected() } }