Manages the pool state without having to loop through relays, saving some milliseconds of processing.

This commit is contained in:
Vitor Pamplona
2025-11-18 17:12:39 -05:00
parent 326f059d1e
commit a36d35912a
9 changed files with 90 additions and 120 deletions
@@ -704,7 +704,7 @@ class Account(
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
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)
}
@@ -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)
@@ -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(
@@ -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<PoolStatus> =
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,
@@ -50,8 +50,8 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<NormalizedRelayUrl> =
account.client
.relayStatusFlow()
.value.available
.availableRelaysFlow()
.value
.sorted()
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
@@ -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)
@@ -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<RelayPool.RelayPoolStatus>
fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
fun connect()
@@ -80,7 +81,9 @@ interface INostrClient {
}
object EmptyNostrClient : INostrClient {
override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus())
override fun connectedRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
override fun availableRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
override fun connect() { }
@@ -320,5 +320,7 @@ class NostrClient(
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId)
override fun relayStatusFlow() = relayPool.statusFlow
override fun connectedRelaysFlow() = relayPool.connectedRelays
override fun availableRelaysFlow() = relayPool.availableRelays
}
@@ -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<NormalizedRelayUrl, IRelayClient>()
// Backing property to avoid flow emissions from other classes
private val _statusFlow = MutableStateFlow<RelayPoolStatus>(RelayPoolStatus())
val statusFlow: StateFlow<RelayPoolStatus> = _statusFlow.asStateFlow()
private val _connectedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
val connectedRelays = _connectedRelays.asStateFlow()
private val _availableRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(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<NormalizedRelayUrl>) {
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<NormalizedRelayUrl> = relays.keys()
fun connectedRelays(): Set<NormalizedRelayUrl> =
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<NormalizedRelayUrl> = emptySet(),
val available: Set<NormalizedRelayUrl> = emptySet(),
val isConnected: Boolean = connected.isNotEmpty(),
)
fun connectedRelaysCount(): Int = relays.count { url, relay -> relay.isConnected() }
}