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> { fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
if (event is MetadataEvent || event is AdvertisedRelayListEvent) { if (event is MetadataEvent || event is AdvertisedRelayListEvent) {
// everywhere // everywhere
return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available return followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value
} }
if (event is GiftWrapEvent) { if (event is GiftWrapEvent) {
val receiver = event.recipientPubKey() val receiver = event.recipientPubKey()
@@ -904,7 +904,7 @@ class Account(
} }
fun sendLiterallyEverywhere(event: Event) { 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) cache.justConsumeMyOwnEvent(event)
} }
@@ -154,7 +154,7 @@ class AccountFollowsLoaderSubAssembler(
println("AccountFollowNeeds ${users.size}") println("AccountFollowNeeds ${users.size}")
val connectedRelays = client.relayStatusFlow().value.connected val connectedRelays = client.connectedRelaysFlow().value
val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried) val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried)
@@ -96,7 +96,7 @@ class UserOutboxFinderSubAssembler(
if (noOutboxList.isEmpty()) return null if (noOutboxList.isEmpty()) return null
val accounts = keys.mapTo(mutableSetOf()) { it.account } val accounts = keys.mapTo(mutableSetOf()) { it.account }
val connectedRelays = client.relayStatusFlow().value.connected val connectedRelays = client.connectedRelaysFlow().value
val perRelayKeysBoth = val perRelayKeysBoth =
pickRelaysToLoadUsers( 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.Font18SP
import com.vitorpamplona.amethyst.ui.theme.IconRowModifier import com.vitorpamplona.amethyst.ui.theme.IconRowModifier
import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier 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.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer 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.bannerModifier
import com.vitorpamplona.amethyst.ui.theme.drawerSpacing import com.vitorpamplona.amethyst.ui.theme.drawerSpacing
import com.vitorpamplona.amethyst.ui.theme.placeholderText 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.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists 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 @Composable
fun DrawerContent( 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 @Composable
fun NavigationRow( fun NavigationRow(
title: Int, title: Int,
@@ -691,35 +658,55 @@ fun IconRowRelays(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() }, .padding(vertical = 15.dp, horizontal = 25.dp)
.clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Row( Icon(
modifier = painter = painterRes(R.drawable.relays, 4),
Modifier contentDescription = stringRes(R.string.relay_setup),
.fillMaxWidth() modifier = Size22Modifier,
.padding(vertical = 15.dp, horizontal = 25.dp), tint = MaterialTheme.colorScheme.onSurface,
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,
)
Text( Text(
modifier = Modifier.padding(start = 16.dp), modifier = IconRowTextModifier,
text = stringRes(id = R.string.relay_setup), text = stringRes(id = R.string.relay_setup),
fontSize = 18.sp, 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 @Composable
fun BottomContent( fun BottomContent(
user: User, user: User,
@@ -50,8 +50,8 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<NormalizedRelayUrl> = override fun getRelayList(): List<NormalizedRelayUrl> =
account.client account.client
.relayStatusFlow() .availableRelaysFlow()
.value.available .value
.sorted() .sorted()
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { 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 IconRowTextModifier = Modifier.padding(start = 16.dp)
val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp) val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp)
val Width16Space = Modifier.width(Size16dp)
val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth() val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth()
val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp) 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.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener 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.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
@@ -32,7 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface INostrClient { interface INostrClient {
fun relayStatusFlow(): StateFlow<RelayPool.RelayPoolStatus> fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
fun connect() fun connect()
@@ -80,7 +81,9 @@ interface INostrClient {
} }
object EmptyNostrClient : 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() { } override fun connect() { }
@@ -320,5 +320,7 @@ class NostrClient(
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId) 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 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.EmptyClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient 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.nip01Core.relay.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.utils.cache.LargeCache import com.vitorpamplona.quartz.utils.cache.LargeCache
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/** /**
* RelayPool manages a collection of Nostr relays, abstracting individual connections and providing * RelayPool manages a collection of Nostr relays, abstracting individual connections and providing
@@ -61,9 +60,11 @@ class RelayPool(
) : IRelayClientListener { ) : IRelayClientListener {
private val relays = LargeCache<NormalizedRelayUrl, IRelayClient>() private val relays = LargeCache<NormalizedRelayUrl, IRelayClient>()
// Backing property to avoid flow emissions from other classes private val _connectedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
private val _statusFlow = MutableStateFlow<RelayPoolStatus>(RelayPoolStatus()) val connectedRelays = _connectedRelays.asStateFlow()
val statusFlow: StateFlow<RelayPoolStatus> = _statusFlow.asStateFlow()
private val _availableRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
val availableRelays = _availableRelays.asStateFlow()
fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url) fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url)
@@ -87,21 +88,18 @@ class RelayPool(
relay.connectAndSyncFiltersIfDisconnected(ignoreRetryDelays) relay.connectAndSyncFiltersIfDisconnected(ignoreRetryDelays)
} }
} }
updateStatus()
} }
fun connect() { fun connect() {
relays.forEach { url, relay -> relays.forEach { url, relay ->
relay.connect() relay.connect()
} }
updateStatus()
} }
fun connectIfDisconnected() { fun connectIfDisconnected() {
relays.forEach { url, relay -> relays.forEach { url, relay ->
relay.connectAndSyncFiltersIfDisconnected() relay.connectAndSyncFiltersIfDisconnected()
} }
updateStatus()
} }
fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected() fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected()
@@ -110,7 +108,6 @@ class RelayPool(
relays.forEach { url, relay -> relays.forEach { url, relay ->
relay.disconnect() relay.disconnect()
} }
updateStatus()
} }
fun sendOrConnectAndSync( fun sendOrConnectAndSync(
@@ -144,7 +141,13 @@ class RelayPool(
// -------------------- // --------------------
// Pool Maintenance // 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) fun createRelayIfAbsent(relay: NormalizedRelayUrl): Boolean = relays.createIfAbsent(relay, ::createNewRelay)
@@ -155,39 +158,40 @@ class RelayPool(
val toRemove = relays.keys() - newRelays val toRemove = relays.keys() - newRelays
var atLeastOne = false var atLeastOne = false
newRelays.forEach { newRelays.forEach { relay ->
if (createRelayIfAbsent(it)) { if (createRelayIfAbsent(relay)) {
atLeastOne = true atLeastOne = true
} }
} }
toRemove.forEach { toRemove.forEach { relay ->
if (removeRelayInner(it)) { if (removeRelayInner(relay)) {
atLeastOne = true atLeastOne = true
} }
} }
if (atLeastOne) { if (atLeastOne) {
updateStatus() _availableRelays.update { relays.keys() }
} }
} }
fun addRelay(relay: NormalizedRelayUrl): IRelayClient { fun addRelay(relay: NormalizedRelayUrl): IRelayClient {
if (createRelayIfAbsent(relay)) { if (createRelayIfAbsent(relay)) {
updateStatus() _availableRelays.update { relays.keys() }
} }
return getOrCreateRelay(relay) return getOrCreateRelay(relay)
} }
fun addAllRelays(relayList: List<NormalizedRelayUrl>) { fun addAllRelays(relayList: List<NormalizedRelayUrl>) {
var atLeastOne = false var atLeastOne = false
relayList.forEach { relayList.forEach { relay ->
if (createRelayIfAbsent(it)) { if (createRelayIfAbsent(relay)) {
atLeastOne = true atLeastOne = true
} }
} }
if (atLeastOne) { if (atLeastOne) {
updateStatus() _availableRelays.update { relays.keys() }
} }
} }
@@ -202,7 +206,7 @@ class RelayPool(
fun removeRelay(relay: NormalizedRelayUrl) { fun removeRelay(relay: NormalizedRelayUrl) {
if (removeRelayInner(relay)) { if (removeRelayInner(relay)) {
updateStatus() _availableRelays.update { relays.keys() }
} }
} }
@@ -210,7 +214,7 @@ class RelayPool(
if (relays.size() > 0) { if (relays.size() > 0) {
disconnect() disconnect()
relays.clear() relays.clear()
updateStatus() _availableRelays.update { emptySet() }
} }
} }
@@ -224,12 +228,12 @@ class RelayPool(
pingMillis: Int, pingMillis: Int,
compressed: Boolean, compressed: Boolean,
) { ) {
updateStatus() _connectedRelays.update { it + relay.url }
listener.onConnected(relay, pingMillis, compressed) listener.onConnected(relay, pingMillis, compressed)
} }
override fun onDisconnected(relay: IRelayClient) { override fun onDisconnected(relay: IRelayClient) {
updateStatus() _connectedRelays.update { it - relay.url }
listener.onDisconnected(relay) listener.onDisconnected(relay)
} }
@@ -251,33 +255,5 @@ class RelayPool(
success: Boolean, success: Boolean,
) = listener.onSent(relay, cmdStr, cmd, success) ) = listener.onSent(relay, cmdStr, cmd, success)
// --------------- fun connectedRelaysCount(): Int = relays.count { url, relay -> relay.isConnected() }
// 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(),
)
} }