- Separates a relay failure tracker to a module

- Updates finding the outbox relay set to include the set of connected relays before all of the relays.
This commit is contained in:
Vitor Pamplona
2025-10-21 15:55:32 -04:00
parent a27df3a9b6
commit e2b2b10994
10 changed files with 201 additions and 187 deletions
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
@@ -192,14 +193,26 @@ class AppModules(
// Tries to verify new OTS events when they arrive.
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
// Tracks if it is possible to connect to relays.
val failureTracker = RelayOfflineTracker(client)
// Captures statistics about relays
val relayStats = RelayStats(client)
// Logs debug messages when needed
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
val relayReqStats = if (isDebug) RelayReqStats(client) else null
val logger = if (isDebug) RelaySpeedLogger(client) else null
// Coordinates all subscriptions for the Nostr Client
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, authCoordinator.receiver, applicationDefaultScope)
val sources: RelaySubscriptionsCoordinator =
RelaySubscriptionsCoordinator(
LocalCache,
client,
authCoordinator.receiver,
failureTracker,
applicationDefaultScope,
)
// keeps all accounts live
val accountsCache =
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProf
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import kotlinx.coroutines.CoroutineScope
@@ -47,10 +48,11 @@ class RelaySubscriptionsCoordinator(
cache: LocalCache,
client: INostrClient,
authenticator: IAuthStatus,
failureTracker: RelayOfflineTracker,
scope: CoroutineScope,
) {
// main one: notifications, dms and account settings
val account = AccountFilterAssembler(client, cache, authenticator, scope)
val account = AccountFilterAssembler(client, cache, authenticator, failureTracker, scope)
// always running, feed assemblers.
val home = HomeFilterAssembler(client)
@@ -62,7 +64,7 @@ class RelaySubscriptionsCoordinator(
// they are active when looking at events, users, channels.
val channelFinder = ChannelFinderFilterAssemblyGroup(client)
val eventFinder = EventFinderFilterAssembler(client)
val userFinder = UserFinderFilterAssembler(client, cache)
val userFinder = UserFinderFilterAssembler(client, cache, failureTracker)
// active when searching or tagging users.
val search = SearchFilterAssembler(client, scope, cache)
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59Gi
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import kotlinx.coroutines.CoroutineScope
@@ -49,12 +50,13 @@ class AccountFilterAssembler(
client: INostrClient,
cache: LocalCache,
authenticator: IAuthStatus,
failureTracker: RelayOfflineTracker,
scope: CoroutineScope,
) : ComposeSubscriptionManager<AccountQueryState>() {
val group =
listOf(
AccountMetadataEoseManager(client, ::allKeys),
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, ::allKeys),
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
AccountGiftWrapsEoseManager(client, ::allKeys),
AccountDraftsEoseManager(client, ::allKeys),
AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys),
@@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follow
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager
@@ -32,6 +30,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
@@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
/**
* This downloads the outbox events for all Follows of all users.
@@ -62,13 +60,12 @@ class AccountFollowsLoaderSubAssembler(
val cache: LocalCache,
val scope: CoroutineScope,
val authStatus: IAuthStatus,
val failureTracker: RelayOfflineTracker,
val allKeys: () -> Set<AccountQueryState>,
) : IEoseManager {
private val logTag = "AccountFollowsLoaderSubAssembler"
private val orchestrator = SubscriptionController(client)
private val cannotConnectRelays = mutableSetOf<NormalizedRelayUrl>()
// Refreshes observers in batches of 500ms
private val bundler = BundledUpdate(500, Dispatchers.Default)
@@ -141,56 +138,28 @@ class AccountFollowsLoaderSubAssembler(
newEose(TimeUtils.now(), relay, forFilters)
}
}
override fun onCannotConnect(
relay: NormalizedRelayUrl,
message: String,
forFilters: List<Filter>?,
) {
cannotConnectRelays.add(relay)
}
},
)
fun updateFilterForAllAccounts(accounts: Collection<Account>): List<RelayBasedFilter>? {
val noOutboxList = mutableSetOf<User>()
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
val users = mutableSetOf<User>()
accounts.forEach { key ->
key.kind3FollowList.userList.value.forEach { user ->
if (user.authorRelayList() == null) {
noOutboxList.add(user)
users.add(user)
}
}
indexRelays.addAll(
key.indexerRelayList.flow.value
.ifEmpty { DefaultIndexerRelayList },
)
homeRelays.addAll(key.nip65RelayList.allFlowNoDefaults.value)
homeRelays.addAll(key.privateStorageRelayList.flow.value)
homeRelays.addAll(key.localRelayList.flow.value)
searchRelays.addAll(key.trustedRelayList.flow.value)
searchRelays.addAll(
key.searchRelayList.flow.value
.ifEmpty { DefaultSearchRelayList },
)
// uses followShared to ignore personal relays when finding users.
commonRelays.addAll(key.followSharedOutboxesOrProxy.flow.value - cannotConnectRelays)
}
if (noOutboxList.isEmpty()) return null
if (users.isEmpty()) return null
val perRelay = pickRelaysToLoadUsers(noOutboxList, indexRelays, homeRelays, searchRelays, commonRelays, hasTried)
println("AccountFollowNeeds ${users.size}")
hasTried.removeEveryoneBut(noOutboxList)
val connectedRelays = client.relayStatusFlow().value.connected
val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried)
hasTried.removeEveryoneBut(users)
return perRelay.mapNotNull { (relay, users) ->
if (users.isNotEmpty()) {
@@ -20,71 +20,62 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.ifEmpty
val MetadataOnlyKinds = listOf(MetadataEvent.KIND)
val OutboxOnlyKinds = listOf(AdvertisedRelayListEvent.KIND)
val BothKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
fun filterFindFollowMetadataForKey(
loadMetadata: Set<User>,
loadOutbox: Set<User>,
connectedRelays: Set<NormalizedRelayUrl>,
indexRelays: Set<NormalizedRelayUrl>,
searchRelays: Set<NormalizedRelayUrl>,
allRelays: Set<NormalizedRelayUrl>,
fun pickRelaysToLoadUsers(
users: Set<User>,
accounts: Collection<Account>,
connected: Set<NormalizedRelayUrl>,
cannotConnectRelays: Set<NormalizedRelayUrl>,
hasTried: EOSEAccountFast<User>,
): List<RelayBasedFilter> {
val both = loadMetadata.intersect(loadOutbox)
val onlyMetadata = loadMetadata - both
val onlyOutbox = loadOutbox - both
): Map<NormalizedRelayUrl, Set<HexKey>> {
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
val perRelayKeysBoth = pickRelaysToLoadUsers(both, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
val perRelayKeysOnlyMetadata = pickRelaysToLoadUsers(onlyMetadata, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
val perRelayKeysOnlyOutbox = pickRelaysToLoadUsers(onlyOutbox, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
accounts.forEach { key ->
indexRelays.addAll(
key.indexerRelayList.flow.value
.ifEmpty { DefaultIndexerRelayList },
)
return perRelayKeysBoth.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = BothKinds, authors = sortedUsers),
)
} else {
null
}
} +
perRelayKeysOnlyMetadata.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = MetadataOnlyKinds, authors = sortedUsers),
)
} else {
null
}
} +
perRelayKeysOnlyOutbox.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = OutboxOnlyKinds, authors = sortedUsers),
)
} else {
null
}
}
homeRelays.addAll(key.nip65RelayList.allFlowNoDefaults.value)
homeRelays.addAll(key.privateStorageRelayList.flow.value)
homeRelays.addAll(key.localRelayList.flow.value)
searchRelays.addAll(key.trustedRelayList.flow.value)
searchRelays.addAll(
key.searchRelayList.flow.value
.ifEmpty { DefaultSearchRelayList },
)
// uses followShared to ignore personal relays when finding users.
commonRelays.addAll(
key.followSharedOutboxesOrProxy.flow.value
.ifEmpty { Constants.eventFinderRelays },
)
}
return pickRelaysToLoadUsers(
users,
indexRelays - cannotConnectRelays,
homeRelays - cannotConnectRelays,
searchRelays - cannotConnectRelays,
connected,
commonRelays - cannotConnectRelays,
hasTried,
)
}
fun pickRelaysToLoadUsers(
@@ -92,6 +83,7 @@ fun pickRelaysToLoadUsers(
indexRelays: Set<NormalizedRelayUrl>,
homeRelays: Set<NormalizedRelayUrl>,
searchRelays: Set<NormalizedRelayUrl>,
connected: Set<NormalizedRelayUrl>,
commonRelays: Set<NormalizedRelayUrl>,
hasTried: EOSEAccountFast<User>,
): Map<NormalizedRelayUrl, Set<HexKey>> =
@@ -166,6 +158,23 @@ fun pickRelaysToLoadUsers(
add(it, key.pubkeyHex)
}
val connectedRelaysLeftToTry =
(connected - tried)
.sortedBy { relay ->
key.pubkeyHex.hashCode() xor relay.url.hashCode()
}.take(100)
// picks one at random to avoid overloading these relays
if (users.size > 300) {
connectedRelaysLeftToTry.take(20).forEach {
add(it, key.pubkeyHex)
}
} else {
connectedRelaysLeftToTry.forEach {
add(it, key.pubkeyHex)
}
}
if (searchRelaysLeftToTry.size < 2) {
// This creates a pre-deterministic order of the array such that
// if this function is called twice, it returns the same arrays
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.Us
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
// This allows multiple screen to be listening to tags, even the same tag
class UserFinderQueryState(
@@ -38,10 +39,11 @@ class UserFinderQueryState(
class UserFinderFilterAssembler(
client: INostrClient,
cache: LocalCache,
failureTracker: RelayOfflineTracker,
) : ComposeSubscriptionManager<UserFinderQueryState>() {
val group =
listOf(
UserOutboxFinderSubAssembler(client, cache, ::allKeys),
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
UserWatcherSubAssembler(client, cache, ::allKeys),
UserReportsSubAssembler(client, ::allKeys),
)
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
val RelayListKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
fun findFindUserOutBox(
authors: Set<User>,
connectedRelays: Set<NormalizedRelayUrl>,
indexRelays: Set<NormalizedRelayUrl>,
searchRelays: Set<NormalizedRelayUrl>,
allRelays: Set<NormalizedRelayUrl>,
hasTried: EOSEAccountFast<User>,
): List<RelayBasedFilter> {
val perRelayKeysBoth = pickRelaysToLoadUsers(authors, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
return perRelayKeysBoth.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = RelayListKinds, authors = sortedUsers),
)
} else {
null
}
}
}
@@ -20,27 +20,32 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.ifEmpty
class UserOutboxFinderSubAssembler(
client: INostrClient,
val cache: LocalCache,
val failureTracker: RelayOfflineTracker,
allKeys: () -> Set<UserFinderQueryState>,
) : BaseEoseManager<UserFinderQueryState>(client, allKeys) {
val relayListKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
/**
* This assembler saves the EOSE per user key. That EOSE includes their metadata, etc
* and reports, but only from trusted accounts (follows of all logged in users).
@@ -91,35 +96,28 @@ class UserOutboxFinderSubAssembler(
if (noOutboxList.isEmpty()) return null
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
val allRelays = mutableSetOf<NormalizedRelayUrl>()
val accounts = keys.mapTo(mutableSetOf()) { it.account }
val connectedRelays = client.relayStatusFlow().value.connected
keys.mapTo(mutableSetOf()) { it.account }.forEach { acc ->
// broadens the search as it goes.
indexRelays.addAll(
acc.indexerRelayList.flow.value
.ifEmpty { DefaultIndexerRelayList },
val perRelayKeysBoth =
pickRelaysToLoadUsers(
noOutboxList,
accounts,
connectedRelays,
failureTracker.cannotConnectRelays,
hasTried,
)
indexRelays.addAll(acc.proxyRelayList.flow.value)
searchRelays.addAll(acc.nip65RelayList.allFlowNoDefaults.value)
searchRelays.addAll(acc.privateStorageRelayList.flow.value)
searchRelays.addAll(acc.localRelayList.flow.value)
searchRelays.addAll(acc.searchRelayList.flow.value)
searchRelays.addAll(acc.trustedRelayList.flow.value)
allRelays.addAll(acc.followOutboxesOrProxy.flow.value)
return perRelayKeysBoth.mapNotNull {
val sortedUsers = it.value.sorted()
if (sortedUsers.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(kinds = relayListKinds, authors = sortedUsers),
)
} else {
null
}
}
allRelays.addAll(Constants.eventFinderRelays)
return findFindUserOutBox(
noOutboxList,
client.relayStatusFlow().value.connected,
indexRelays,
searchRelays,
allRelays,
hasTried,
)
}
}
@@ -98,6 +98,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
@@ -1678,6 +1679,7 @@ fun mockAccountViewModel(): AccountViewModel {
val authenticator = EmptyIAuthStatus
val nwcFilters = NWCPaymentFilterAssembler(client)
val failureTracker = RelayOfflineTracker(client)
val account =
Account(
@@ -1696,7 +1698,7 @@ fun mockAccountViewModel(): AccountViewModel {
settings = uiState,
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
).also {
mockedCache = it
}
@@ -1727,6 +1729,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
val authenticator = EmptyIAuthStatus
val nwcFilters = NWCPaymentFilterAssembler(client)
val failureTracker = RelayOfflineTracker(client)
val account =
Account(
@@ -1745,7 +1748,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
settings = uiState,
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
).also {
vitorCache = it
}
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.Log
/**
* Listens to NostrClient's onNotify messages from the relay
*/
class RelayOfflineTracker(
val client: INostrClient,
) {
companion object {
const val TAG = "RelayOfflineTracker"
}
val cannotConnectRelays = mutableSetOf<NormalizedRelayUrl>()
private val clientListener =
object : IRelayClientListener {
override fun onConnected(
relay: IRelayClient,
pingMillis: Int,
compressed: Boolean,
) {
cannotConnectRelays.remove(relay.url)
}
override fun onCannotConnect(
relay: IRelayClient,
errorMessage: String,
) {
cannotConnectRelays.add(relay.url)
}
}
init {
Log.d(TAG, "Init, Subscribe")
client.subscribe(clientListener)
}
fun destroy() {
// makes sure to run
Log.d(TAG, "Destroy, Unsubscribe")
client.unsubscribe(clientListener)
}
}