diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index f0160d9e7..cfcdc8fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -21,25 +21,29 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache.addressables import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.flatten class EventWatcherSubAssembler( client: NostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { var lastNotesOnFilter = emptyList() + var latestEOSEs: EOSEAccountFast = EOSEAccountFast(10000) override fun newEose( relayUrl: String, time: Long, ) { lastNotesOnFilter.forEach { - it.lastReactionsDownloadTime.newEose(relayUrl, time) + latestEOSEs.newEose(it, relayUrl, time) } super.newEose(relayUrl, time) } @@ -53,18 +57,53 @@ class EventWatcherSubAssembler( } lastNotesOnFilter = keys.map { it.note } - val addressables = lastNotesOnFilter.filterIsInstance() - val events = keys.mapNotNull { if (it.note !is AddressableNote) it.note else null } - return groupByEOSEPresence(lastNotesOnFilter) - .map { - listOfNotNull( - filterRepliesAndReactionsToNotes(events), - filterRepliesAndReactionsToAddresses(addressables), - filterQuotesToNotes(it), - ).flatten() + return groupByRelayPresence(lastNotesOnFilter, latestEOSEs) + .map { group -> + if (group.isNotEmpty()) { + val addressables = group.filterIsInstance() + val events = group.mapNotNull { if (it !is AddressableNote) it else null } + + listOfNotNull( + filterRepliesAndReactionsToNotes(events, findMinimumEOSEs(events, latestEOSEs)), + filterRepliesAndReactionsToAddresses(addressables, findMinimumEOSEs(addressables, latestEOSEs)), + filterQuotesToNotes(group, findMinimumEOSEs(group, latestEOSEs)), + ).flatten() + } else { + emptyList() + } }.flatten() } override fun distinct(key: EventFinderQueryState) = key.note + + fun groupByRelayPresence( + notes: Iterable, + eoseCache: EOSEAccountFast, + ): Collection> = + notes.groupBy { eoseCache.since(it)?.keys?.hashCode() } + .values.map { + // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again + it.sortedBy { it.idHex } + } + + fun findMinimumEOSEs( + notes: List, + eoseCache: EOSEAccountFast, + ): Map { + val minLatestEOSEs = mutableMapOf() + + notes.forEach { note -> + eoseCache.since(note)?.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, EOSETime(it.value.time)) + } else if (it.value.time < minEose.time) { + minEose.time = it.value.time + } + } + } + + return minLatestEOSEs + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt index 5b5ead18f..8770b335b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt @@ -23,11 +23,15 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -fun filterQuotesToNotes(keys: List): List? { - if (keys.isEmpty()) return null +fun filterQuotesToNotes( + notes: List, + since: Map?, +): List? { + if (notes.isEmpty()) return null return listOf( TypedFilter( @@ -35,8 +39,8 @@ fun filterQuotesToNotes(keys: List): List? { filter = SincePerRelayFilter( kinds = listOf(TextNoteEvent.KIND), - tags = mapOf("q" to keys.map { it.idHex }), - since = findMinimumEOSEs(keys), + tags = mapOf("q" to notes.map { it.idHex }.sorted()), + since = since, // Max amount of "replies" to download on a specific event. limit = 1000, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt index 7d3926800..244d613c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -35,10 +36,13 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -fun filterRepliesAndReactionsToAddresses(keys: List): List? { +fun filterRepliesAndReactionsToAddresses( + keys: List, + since: Map?, +): List? { if (keys.isEmpty()) return null - val addresses = keys.map { it.address().toValue() } + val addresses = keys.mapTo(mutableSetOf()) { it.address().toValue() }.sorted() return listOf( TypedFilter( @@ -58,7 +62,7 @@ fun filterRepliesAndReactionsToAddresses(keys: List): List): List): List? { - if (keys.isEmpty()) return null +fun filterRepliesAndReactionsToNotes( + events: List, + since: Map?, +): List? { + if (events.isEmpty()) return null + + val eventIds = events.mapTo(mutableSetOf()) { it.idHex }.sorted() return listOf( TypedFilter( @@ -60,8 +66,8 @@ fun filterRepliesAndReactionsToNotes(keys: List): List? { TextNoteModificationEvent.KIND, GitReplyEvent.KIND, ), - tags = mapOf("e" to keys.map { it.idHex }), - since = findMinimumEOSEs(keys), + tags = mapOf("e" to eventIds), + since = since, // Max amount of "replies" to download on a specific event. limit = 1000, ), @@ -77,8 +83,8 @@ fun filterRepliesAndReactionsToNotes(keys: List): List? { NIP90StatusEvent.KIND, TorrentCommentEvent.KIND, ), - tags = mapOf("e" to keys.map { it.idHex }), - since = findMinimumEOSEs(keys), + tags = mapOf("e" to eventIds), + since = since, limit = 100, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt deleted file mode 100644 index 9d4c1ff9c..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt +++ /dev/null @@ -1,52 +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.event.watchers - -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.ammolite.relays.filters.EOSETime - -fun groupByEOSEPresence(notes: Iterable): Collection> = - notes - .groupBy { - it.lastReactionsDownloadTime.relayList.keys - .sorted() - .joinToString(",") - }.values - .map { - it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again - } - -fun findMinimumEOSEs(notes: List): Map { - val minLatestEOSEs = mutableMapOf() - - notes.forEach { note -> - note.lastReactionsDownloadTime.relayList.forEach { - val minEose = minLatestEOSEs[it.key] - if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time - } - } - } - - return minLatestEOSEs -}