Merge pull request #1870 from vitorpamplona/claude/fix-event-counter-per-filter-Aps6R

Add per-filter event counting for relay pagination
This commit is contained in:
Vitor Pamplona
2026-03-17 13:46:10 -04:00
committed by GitHub
2 changed files with 28 additions and 3 deletions
@@ -45,7 +45,6 @@ import kotlinx.coroutines.sync.Semaphore
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.coroutineContext
/** /**
* Syncs the user's events across all known relays: * Syncs the user's events across all known relays:
@@ -40,6 +40,11 @@ import kotlin.coroutines.coroutineContext
* After EOSE the oldest [Event.createdAt] seen in that page minus one becomes the * After EOSE the oldest [Event.createdAt] seen in that page minus one becomes the
* next `until`, and the query repeats until the relay returns no new events. * next `until`, and the query repeats until the relay returns no new events.
* *
* Event counting is tracked per filter using [Filter.match]. A filter is considered
* fulfilled when the number of matching events reaches its [Filter.limit]. Pagination
* stops when all filters with limits are fulfilled or when a page returns no events.
* Filters without a limit are considered unbounded and only stop on empty pages.
*
* @param relay The relay to query. * @param relay The relay to query.
* @param baseFilters Filters to apply on every page (the `until` field is overwritten per page). * @param baseFilters Filters to apply on every page (the `until` field is overwritten per page).
* @param timeoutMs Maximum time to wait for a single page's EOSE before giving up. * @param timeoutMs Maximum time to wait for a single page's EOSE before giving up.
@@ -55,18 +60,32 @@ suspend fun INostrClient.downloadFromRelay(
var until: Long? = null var until: Long? = null
var totalEvents = 0 var totalEvents = 0
// Track how many matching events each filter has received so far.
val matchCountPerFilter = IntArray(baseFilters.size)
while (true) { while (true) {
coroutineContext.ensureActive() coroutineContext.ensureActive()
// Only include filters that still need more events.
val activeFilterIndices =
baseFilters.indices.filter { i ->
val limit = baseFilters[i].limit
limit == null || matchCountPerFilter[i] < limit
}
if (activeFilterIndices.isEmpty()) break
val activeBaseFilters = activeFilterIndices.map { baseFilters[it] }
val eventChannel = Channel<Event>(UNLIMITED) val eventChannel = Channel<Event>(UNLIMITED)
val doneChannel = Channel<Unit>(Channel.CONFLATED) val doneChannel = Channel<Unit>(Channel.CONFLATED)
val subId = newSubId() val subId = newSubId()
val filters = val filters =
if (until == null) { if (until == null) {
baseFilters activeBaseFilters
} else { } else {
baseFilters.map { it.copy(until = until) } activeBaseFilters.map { it.copy(until = until) }
} }
val listener = val listener =
@@ -116,6 +135,13 @@ suspend fun INostrClient.downloadFromRelay(
onEvent(event) onEvent(event)
pageCount++ pageCount++
if (event.createdAt < pageMinTs) pageMinTs = event.createdAt if (event.createdAt < pageMinTs) pageMinTs = event.createdAt
// Count this event against every base filter it matches.
for (i in baseFilters.indices) {
if (baseFilters[i].match(event)) {
matchCountPerFilter[i]++
}
}
} }
if (pageCount == 0) break if (pageCount == 0) break