Finalizes the RelaySync utility
This commit is contained in:
+3
-1
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface INostrClient {
|
||||
interface INostrClient : AutoCloseable {
|
||||
fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
@@ -136,4 +136,6 @@ object EmptyNostrClient : INostrClient {
|
||||
override fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>> = emptyMap()
|
||||
|
||||
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
+6
-1
@@ -79,7 +79,8 @@ class NostrClient(
|
||||
private val websocketBuilder: WebsocketBuilder,
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
) : INostrClient,
|
||||
IRelayClientListener {
|
||||
IRelayClientListener,
|
||||
AutoCloseable {
|
||||
private val relayPool: RelayPool = RelayPool(websocketBuilder, this)
|
||||
|
||||
private val activeRequests: PoolRequests = PoolRequests()
|
||||
@@ -326,4 +327,8 @@ class NostrClient(
|
||||
override fun connectedRelaysFlow() = relayPool.connectedRelays
|
||||
|
||||
override fun availableRelaysFlow() = relayPool.availableRelays
|
||||
|
||||
override fun close() {
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
+30
-32
@@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.coroutines.coroutineContext
|
||||
@@ -63,6 +62,8 @@ suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
// Track how many matching events each filter has received so far.
|
||||
val matchCountPerFilter = IntArray(filters.size)
|
||||
|
||||
val subId = newSubId()
|
||||
|
||||
while (true) {
|
||||
coroutineContext.ensureActive()
|
||||
|
||||
@@ -75,9 +76,7 @@ suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
|
||||
if (remainingFilters.isEmpty()) break
|
||||
|
||||
val eventChannel = Channel<Event>(UNLIMITED)
|
||||
val doneChannel = Channel<Unit>(Channel.CONFLATED)
|
||||
val subId = newSubId()
|
||||
|
||||
val activeFilters =
|
||||
if (until == null) {
|
||||
@@ -86,19 +85,37 @@ suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
remainingFilters.map { it.copy(until = until) }
|
||||
}
|
||||
|
||||
var pageCount = 0
|
||||
var pageMinTs = Long.MAX_VALUE
|
||||
|
||||
val listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eventChannel.trySend(event)
|
||||
onEvent(event)
|
||||
pageCount++
|
||||
if (event.createdAt < pageMinTs) pageMinTs = event.createdAt
|
||||
|
||||
// Count this event against every base filter it matches.
|
||||
if (matchCountPerFilter.size == 1) {
|
||||
// no need to run the match.
|
||||
matchCountPerFilter[0]++
|
||||
} else {
|
||||
for (i in filters.indices) {
|
||||
val limit = filters[i].limit
|
||||
if ((limit == null || matchCountPerFilter[i] < limit) && filters[i].match(event)) {
|
||||
matchCountPerFilter[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
doneChannel.trySend(Unit)
|
||||
@@ -106,49 +123,30 @@ suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
|
||||
override fun onClosed(
|
||||
message: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: NormalizedRelayUrl,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
println("AABBCC $message")
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
openReqSubscription(subId, mapOf(relay to activeFilters), listener)
|
||||
withTimeoutOrNull(timeoutMs) { doneChannel.receive() }
|
||||
close(subId)
|
||||
eventChannel.close()
|
||||
doneChannel.close()
|
||||
|
||||
var pageCount = 0
|
||||
var pageMinTs = Long.MAX_VALUE
|
||||
for (event in eventChannel) {
|
||||
onEvent(event)
|
||||
pageCount++
|
||||
if (event.createdAt < pageMinTs) pageMinTs = event.createdAt
|
||||
|
||||
// Count this event against every base filter it matches.
|
||||
if (matchCountPerFilter.size == 1) {
|
||||
// no need to run the match.
|
||||
matchCountPerFilter[0]++
|
||||
} else {
|
||||
for (i in filters.indices) {
|
||||
val limit = filters[i].limit
|
||||
if ((limit == null || matchCountPerFilter[i] < limit) && filters[i].match(event)) {
|
||||
matchCountPerFilter[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
withTimeoutOrNull(timeoutMs) {
|
||||
doneChannel.receive()
|
||||
}
|
||||
|
||||
close(subId)
|
||||
doneChannel.close()
|
||||
|
||||
if (pageCount == 0) break
|
||||
|
||||
totalEvents += pageCount
|
||||
|
||||
+24
-1
@@ -67,7 +67,7 @@ suspend fun INostrClient.downloadFirstEvent(
|
||||
subscriptionId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
): Event? {
|
||||
val resultChannel = Channel<Event>(UNLIMITED)
|
||||
val resultChannel = Channel<Event?>(UNLIMITED)
|
||||
|
||||
val listener =
|
||||
object : IRequestListener {
|
||||
@@ -79,6 +79,29 @@ suspend fun INostrClient.downloadFirstEvent(
|
||||
) {
|
||||
resultChannel.trySend(event)
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
|
||||
override fun onClosed(
|
||||
message: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
}
|
||||
|
||||
openReqSubscription(subscriptionId, filters, listener)
|
||||
|
||||
+55
-15
@@ -28,26 +28,63 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.compareTo
|
||||
|
||||
class PoolEventOutbox {
|
||||
private var eventOutbox = mapOf<HexKey, PoolEventOutboxState>()
|
||||
val relays = MutableStateFlow(setOf<NormalizedRelayUrl>())
|
||||
|
||||
fun updateRelays() {
|
||||
val myRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
eventOutbox.values.forEach {
|
||||
myRelays.addAll(it.relaysLeft())
|
||||
fun needsToUpdateRelays(): Boolean {
|
||||
val currentRelays = relays.value
|
||||
|
||||
var relaysToRemoveCounter = 0
|
||||
|
||||
currentRelays.forEach { currentRelay ->
|
||||
if (eventOutbox.values.none { currentRelay in it.relaysRemaining }) {
|
||||
relaysToRemoveCounter++
|
||||
}
|
||||
}
|
||||
|
||||
if (relays.value != myRelays) {
|
||||
relays.tryEmit(myRelays)
|
||||
var relaysToAddCounter = 0
|
||||
eventOutbox.values.forEach { outboxState ->
|
||||
if (outboxState.relaysRemaining.any { it !in currentRelays }) {
|
||||
relaysToAddCounter++
|
||||
}
|
||||
}
|
||||
|
||||
return relaysToRemoveCounter > 0 || relaysToAddCounter > 0
|
||||
}
|
||||
|
||||
fun updateRelays() {
|
||||
if (needsToUpdateRelays()) {
|
||||
relays.update { currentRelays ->
|
||||
val relaysToRemove = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
currentRelays.forEach { currentRelay ->
|
||||
if (eventOutbox.values.none { currentRelay in it.relaysRemaining }) {
|
||||
relaysToRemove.add(currentRelay)
|
||||
}
|
||||
}
|
||||
|
||||
val relaysToAdd = mutableSetOf<NormalizedRelayUrl>()
|
||||
eventOutbox.values.forEach { outboxState ->
|
||||
outboxState.relaysRemaining.forEach { relay ->
|
||||
if (relay !in relaysToAdd && relay !in currentRelays) {
|
||||
relaysToAdd.add(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(currentRelays - relaysToRemove) + relaysToAdd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activeOutboxCacheFor(url: NormalizedRelayUrl): Set<HexKey> {
|
||||
val myEvents = mutableSetOf<HexKey>()
|
||||
eventOutbox.forEach { (eventId, outboxCache) ->
|
||||
if (url in outboxCache.relays) {
|
||||
if (url in outboxCache.relaysRemaining) {
|
||||
myEvents.add(eventId)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +109,12 @@ class PoolEventOutbox {
|
||||
id: HexKey,
|
||||
url: NormalizedRelayUrl,
|
||||
) {
|
||||
eventOutbox[id]?.newTry(url)
|
||||
val waiting = eventOutbox[id]
|
||||
waiting?.newTry(url)
|
||||
if (waiting?.isDone() == true) {
|
||||
eventOutbox = eventOutbox - waiting.event.id
|
||||
updateRelays()
|
||||
}
|
||||
}
|
||||
|
||||
fun newResponse(
|
||||
@@ -84,15 +126,13 @@ class PoolEventOutbox {
|
||||
val waiting = eventOutbox[id]
|
||||
if (waiting != null) {
|
||||
waiting.newResponse(url, success, message)
|
||||
clear()
|
||||
if (waiting.isDone()) {
|
||||
eventOutbox = eventOutbox - waiting.event.id
|
||||
updateRelays()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
eventOutbox = eventOutbox.filter { !it.value.isDone() }
|
||||
updateRelays()
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// State management functions
|
||||
// --------------------------
|
||||
@@ -143,7 +183,7 @@ class PoolEventOutbox {
|
||||
errorMessage: String,
|
||||
) {
|
||||
eventOutbox.forEach {
|
||||
if (relay in it.value.relays) {
|
||||
if (relay in it.value.relaysRemaining) {
|
||||
newResponse(it.key, relay, false, errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-30
@@ -26,35 +26,37 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class PoolEventOutboxState(
|
||||
val event: Event,
|
||||
var relays: Set<NormalizedRelayUrl>,
|
||||
var relaysRemaining: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
private var tries = mapOf<NormalizedRelayUrl, Tries>()
|
||||
private var failures = mapOf<NormalizedRelayUrl, Tries>()
|
||||
|
||||
fun updateRelays(newRelays: Set<NormalizedRelayUrl>) {
|
||||
relays = newRelays
|
||||
relaysRemaining = newRelays
|
||||
}
|
||||
|
||||
fun isDone(url: NormalizedRelayUrl) = tries[url]?.isDone() ?: false
|
||||
fun isDone() = relaysRemaining.isEmpty()
|
||||
|
||||
fun isDone() = relays.all { isDone(it) }
|
||||
fun relaysLeft(): Set<NormalizedRelayUrl> = relaysRemaining
|
||||
|
||||
fun relaysLeft(): Set<NormalizedRelayUrl> = relays.filterTo(mutableSetOf()) { !isDone(it) }
|
||||
|
||||
fun isSupposedToGo(url: NormalizedRelayUrl) = url in relays && !isDone(url)
|
||||
fun isSupposedToGo(url: NormalizedRelayUrl) = url in relaysRemaining
|
||||
|
||||
fun forEachUnsentEvent(
|
||||
url: NormalizedRelayUrl,
|
||||
run: (url: Event) -> Unit,
|
||||
) = if (isSupposedToGo(url)) run(event) else null
|
||||
|
||||
fun remainingRelays() = relays.filterTo(mutableSetOf(), ::isSupposedToGo)
|
||||
fun remainingRelays() = relaysRemaining
|
||||
|
||||
fun newTry(url: NormalizedRelayUrl) {
|
||||
val currentTries = tries[url]
|
||||
val currentTries = failures[url]
|
||||
if (currentTries != null) {
|
||||
currentTries.addTriedTime(TimeUtils.now())
|
||||
if (currentTries.isDone()) {
|
||||
relaysRemaining = relaysRemaining - url
|
||||
failures = failures - url
|
||||
}
|
||||
} else {
|
||||
tries = tries + (url to Tries(listOf(TimeUtils.now())))
|
||||
failures = failures + (url to Tries(listOf(TimeUtils.now())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,38 +65,44 @@ class PoolEventOutboxState(
|
||||
success: Boolean,
|
||||
message: String,
|
||||
) {
|
||||
val currentTries = tries[url]
|
||||
if (currentTries != null) {
|
||||
currentTries.addResponse(Response(success, message))
|
||||
val currentTries = failures[url]
|
||||
if (success || message.shouldDiscard()) {
|
||||
relaysRemaining = relaysRemaining - url
|
||||
failures = failures - url
|
||||
} else {
|
||||
tries = tries + (
|
||||
url to
|
||||
Tries(
|
||||
listOf(TimeUtils.now() - 1),
|
||||
listOf(Response(success, message)),
|
||||
)
|
||||
)
|
||||
if (currentTries != null) {
|
||||
currentTries.addResponse(message)
|
||||
} else {
|
||||
failures = failures + (
|
||||
url to
|
||||
Tries(
|
||||
listOf(TimeUtils.now() - 1),
|
||||
listOf(message),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.shouldDiscard() =
|
||||
this.startsWith("replaced:") ||
|
||||
this.startsWith("pow:") ||
|
||||
this.startsWith("deleted:") ||
|
||||
this.startsWith("invalid:")
|
||||
|
||||
// Tries 3 times
|
||||
class Tries(
|
||||
var tries: List<Long> = listOf(),
|
||||
var responses: List<Response> = listOf(),
|
||||
var responses: List<String> = listOf(),
|
||||
) {
|
||||
fun isDone() = responses.any { it.success } || responses.size > 2 || tries.size > 3
|
||||
fun isDone() = responses.size > 2 || tries.size > 3
|
||||
|
||||
fun addResponse(r: Response) {
|
||||
responses += r
|
||||
fun addResponse(msg: String) {
|
||||
responses += msg
|
||||
}
|
||||
|
||||
fun addTriedTime(tried: Long) {
|
||||
tries += tried
|
||||
}
|
||||
}
|
||||
|
||||
class Response(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -42,6 +42,8 @@ class RelayStats(
|
||||
override fun create(key: NormalizedRelayUrl): RelayStat = RelayStat()
|
||||
}
|
||||
|
||||
fun snapshot(): Map<NormalizedRelayUrl, RelayStat> = innerCache.snapshot()
|
||||
|
||||
fun get(url: NormalizedRelayUrl): RelayStat = innerCache[url] ?: throw IllegalArgumentException("Should never happen")
|
||||
|
||||
private val clientListener =
|
||||
|
||||
+2
@@ -101,6 +101,8 @@ private class TrackingNostrClient : INostrClient {
|
||||
override fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>> = emptyMap()
|
||||
|
||||
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<String> = emptySet()
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user