Unify outbox calculation in the RelayOutbox loader.

This commit is contained in:
Vitor Pamplona
2025-09-03 16:51:27 -04:00
parent 43c9df23c4
commit e4e52ec4e6
7 changed files with 79 additions and 140 deletions
@@ -20,18 +20,14 @@
*/ */
package com.vitorpamplona.amethyst.model.nip02FollowLists package com.vitorpamplona.amethyst.model.nip02FollowLists
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -52,37 +48,16 @@ class FollowListOutboxOrProxyRelays(
val cache: LocalCache, val cache: LocalCache,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
combine(flows) { relayListNotes: Array<NoteState> ->
relayListNotes
.mapNotNull {
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
}.flatten()
.toSet()
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> = val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> =
kind3Follows.flow kind3Follows.flow
.transformLatest { .transformLatest { follows ->
emitAll(combineAllFlows(allRelayListFlows(it.authors))) emitAll(
OutboxRelayLoader(true).toAuthorsPerRelayFlow(follows.authors, cache) { it.keys },
)
}.onStart { }.onStart {
emit( emit(
kind3Follows.flow.value.authors OutboxRelayLoader(true).authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it.keys },
.mapNotNull {
getNIP65RelayList(it)?.writeRelaysNorm()
}.flatten()
.toSet(),
) )
}.distinctUntilChanged() }.distinctUntilChanged()
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
@@ -20,20 +20,15 @@
*/ */
package com.vitorpamplona.amethyst.model.nip02FollowLists package com.vitorpamplona.amethyst.model.nip02FollowLists
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -53,51 +48,16 @@ class FollowsPerOutboxRelay(
val cache: LocalCache, val cache: LocalCache,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
fun combineAllRelayListFlows(flows: List<StateFlow<NoteState>>): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
combine(flows) { relayListNotes: Array<NoteState> ->
mapOfSet {
relayListNotes.forEach { noteState ->
noteState.note.author?.pubkeyHex?.let { authorHex ->
val outboxRelayList =
getNIP65RelayList(authorHex)?.writeRelaysNorm()
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
?: Constants.eventFinderRelays
outboxRelayList.forEach { relay ->
add(relay, authorHex)
}
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val outboxPerRelayFlow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> = val outboxPerRelayFlow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
kind3Follows.flow kind3Follows.flow
.transformLatest { .transformLatest {
emitAll(combineAllRelayListFlows(allRelayListFlows(it.authors))) emitAll(
OutboxRelayLoader().toAuthorsPerRelayFlow(it.authors, cache) { it },
)
}.onStart { }.onStart {
emit( emit(
mapOfSet { OutboxRelayLoader().authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
kind3Follows.flow.value.authors.map { authorHex ->
val outboxRelayList =
getNIP65RelayList(authorHex)?.writeRelaysNorm()
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
?: Constants.eventFinderRelays
outboxRelayList.forEach { relay ->
add(relay, authorHex)
}
}
},
) )
}.distinctUntilChanged() }.distinctUntilChanged()
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
@@ -32,75 +32,79 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
class OutboxRelayLoader { class OutboxRelayLoader(
companion object { val rawOutboxRelays: Boolean = false,
private fun authorsPerRelay( ) {
outboxRelayNotes: Array<NoteState>, fun authorsPerRelay(
cache: LocalCache, outboxRelayNotes: Array<NoteState>,
): Map<NormalizedRelayUrl, Set<HexKey>> = cache: LocalCache,
mapOfSet { ): Map<NormalizedRelayUrl, Set<HexKey>> =
outboxRelayNotes.forEach { outboxNote -> mapOfSet {
val note = outboxNote.note outboxRelayNotes.forEach { outboxNote ->
val note = outboxNote.note
val authorHex = val authorHex =
if (note is AddressableNote) { if (note is AddressableNote) {
note.address.pubKeyHex note.address.pubKeyHex
} else {
note.author?.pubkeyHex
}
if (authorHex != null) {
val relays =
if (rawOutboxRelays) {
(outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() ?: emptySet()
} else { } else {
note.author?.pubkeyHex
}
if (authorHex != null) {
val relays =
(outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
?: cache.relayHints.hintsForKey(authorHex).ifEmpty { null } ?: cache.relayHints.hintsForKey(authorHex).ifEmpty { null }
?: Constants.eventFinderRelays ?: Constants.eventFinderRelays
}
relays.forEach { relays.forEach {
if (!it.url.startsWith("wss://feeds.nostr.band") && if (!it.url.startsWith("wss://feeds.nostr.band") &&
!it.url.startsWith("wss://filter.nostr.wine") && !it.url.startsWith("wss://filter.nostr.wine") &&
!it.url.startsWith("wss://nwc.primal.net") && !it.url.startsWith("wss://nwc.primal.net") &&
!it.url.startsWith("wss://relay.getalby.com") !it.url.startsWith("wss://relay.getalby.com")
) { ) {
add(it, authorHex) add(it, authorHex)
}
} }
} }
} }
} }
fun <T> authorsPerRelaySnapshot(
authors: Set<HexKey>,
cache: LocalCache,
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
): T {
val noteMetadata =
authors
.map { pubkeyHex ->
cache
.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
.flow()
.metadata.stateFlow.value
}.toTypedArray()
return transformation(authorsPerRelay(noteMetadata, cache))
} }
fun <T> toAuthorsPerRelayFlow( fun <T> authorsPerRelaySnapshot(
authors: Set<HexKey>, authors: Set<HexKey>,
cache: LocalCache, cache: LocalCache,
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T, transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
): Flow<T> { ): T {
val noteMetadataFlows = val noteMetadata =
authors.map { pubkeyHex -> authors
val note = cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)) .map { pubkeyHex ->
note.flow().metadata.stateFlow cache
} .getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
.flow()
.metadata.stateFlow.value
}.toTypedArray()
return transformation(authorsPerRelay(noteMetadata, cache))
}
return if (noteMetadataFlows.isEmpty()) { fun <T> toAuthorsPerRelayFlow(
MutableStateFlow(transformation(emptyMap())) authors: Set<HexKey>,
} else { cache: LocalCache,
combine(noteMetadataFlows) { outboxRelays -> transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
transformation(authorsPerRelay(outboxRelays, cache)) ): Flow<T> {
} val noteMetadataFlows =
authors.map { pubkeyHex ->
val note = cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
note.flow().metadata.stateFlow
}
return if (noteMetadataFlows.isEmpty()) {
MutableStateFlow(transformation(emptyMap()))
} else {
combine(noteMetadataFlows) { outboxRelays ->
transformation(authorsPerRelay(outboxRelays, cache))
} }
} }
} }
@@ -81,7 +81,7 @@ class AllFollowsByOutboxTopNavFilter(
override fun toPerRelayFlow(cache: LocalCache): Flow<AllFollowsTopNavPerRelayFilterSet> { override fun toPerRelayFlow(cache: LocalCache): Flow<AllFollowsTopNavPerRelayFilterSet> {
val authorsPerRelay = val authorsPerRelay =
if (authors != null) { if (authors != null) {
OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it }
} else { } else {
MutableStateFlow(emptyMap()) MutableStateFlow(emptyMap())
} }
@@ -112,7 +112,7 @@ class AllFollowsByOutboxTopNavFilter(
override fun startValue(cache: LocalCache): AllFollowsTopNavPerRelayFilterSet { override fun startValue(cache: LocalCache): AllFollowsTopNavPerRelayFilterSet {
val authorsPerRelay = val authorsPerRelay =
if (authors != null) { if (authors != null) {
OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it }
} else { } else {
emptyMap() emptyMap()
} }
@@ -52,7 +52,7 @@ class AuthorsByOutboxTopNavFilter(
) )
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsTopNavPerRelayFilterSet> { override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsTopNavPerRelayFilterSet> {
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it }
return combine(authorsPerRelay, blockedRelays) { authors, blocked -> return combine(authorsPerRelay, blockedRelays) { authors, blocked ->
convert(authors.minus(blocked)) convert(authors.minus(blocked))
@@ -60,7 +60,7 @@ class AuthorsByOutboxTopNavFilter(
} }
override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet { override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet {
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it }
return convert(authorsPerRelay.minus(blockedRelays.value)) return convert(authorsPerRelay.minus(blockedRelays.value))
} }
@@ -67,7 +67,7 @@ class SingleCommunityTopNavFilter(
if (authors != null) { if (authors != null) {
// go by authors // go by authors
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it }
return combine(authorsPerRelay, blockedRelays) { authorsPerRelay, blocked -> return combine(authorsPerRelay, blockedRelays) { authorsPerRelay, blocked ->
SingleCommunityTopNavPerRelayFilterSet( SingleCommunityTopNavPerRelayFilterSet(
@@ -100,7 +100,7 @@ class SingleCommunityTopNavFilter(
if (authors != null) { if (authors != null) {
// go by authors // go by authors
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it }
return SingleCommunityTopNavPerRelayFilterSet( return SingleCommunityTopNavPerRelayFilterSet(
authorsPerRelay.minus(blockedRelays.value).mapValues { authorsPerRelay.minus(blockedRelays.value).mapValues {
@@ -52,7 +52,7 @@ class MutedAuthorsByOutboxTopNavFilter(
) )
override fun toPerRelayFlow(cache: LocalCache): Flow<MutedAuthorsTopNavPerRelayFilterSet> { override fun toPerRelayFlow(cache: LocalCache): Flow<MutedAuthorsTopNavPerRelayFilterSet> {
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it }
return combine(authorsPerRelay, blockedRelays) { authors, blocked -> return combine(authorsPerRelay, blockedRelays) { authors, blocked ->
convert(authors.minus(blocked)) convert(authors.minus(blocked))
@@ -60,7 +60,7 @@ class MutedAuthorsByOutboxTopNavFilter(
} }
override fun startValue(cache: LocalCache): MutedAuthorsTopNavPerRelayFilterSet { override fun startValue(cache: LocalCache): MutedAuthorsTopNavPerRelayFilterSet {
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } val authorsPerRelay = OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it }
return convert(authorsPerRelay.minus(blockedRelays.value)) return convert(authorsPerRelay.minus(blockedRelays.value))
} }