From 76c00dea3c62c4ed11fb9140f0851017a2567267 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 17:41:40 +0000 Subject: [PATCH 1/2] fix: track event counts per filter in downloadFromRelay Change the event counter from a single global counter to per-filter tracking using Filter.match(). Each filter's limit is now respected individually - fulfilled filters are excluded from subsequent pages, and pagination stops when all filters with limits are satisfied. https://claude.ai/code/session_01Baaira4xoEHEsaSQCX52Sy --- .../NostrClientDownloadFromRelayExt.kt | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientDownloadFromRelayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientDownloadFromRelayExt.kt index ff099b595..38e180c21 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientDownloadFromRelayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientDownloadFromRelayExt.kt @@ -40,6 +40,11 @@ import kotlin.coroutines.coroutineContext * 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. * + * 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 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. @@ -55,18 +60,32 @@ suspend fun INostrClient.downloadFromRelay( var until: Long? = null var totalEvents = 0 + // Track how many matching events each filter has received so far. + val matchCountPerFilter = IntArray(baseFilters.size) + while (true) { 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(UNLIMITED) val doneChannel = Channel(Channel.CONFLATED) val subId = newSubId() val filters = if (until == null) { - baseFilters + activeBaseFilters } else { - baseFilters.map { it.copy(until = until) } + activeBaseFilters.map { it.copy(until = until) } } val listener = @@ -116,6 +135,13 @@ suspend fun INostrClient.downloadFromRelay( onEvent(event) pageCount++ 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 From 308e9fcb9e355e8106b9cd4adf0b79d08c633f2a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 17:43:04 +0000 Subject: [PATCH 2/2] chore: remove unused coroutineContext import in EventSync https://claude.ai/code/session_01Baaira4xoEHEsaSQCX52Sy --- .../amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt index 7e2854a16..16e06b8f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.sync.Semaphore import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong -import kotlin.coroutines.coroutineContext /** * Syncs the user's events across all known relays: