From 4d31cd7d25f049393ba9c4fcffe071b7ec2ef00d Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 6 Aug 2026 11:02:09 +0300 Subject: [PATCH] refactor: move per-note event finder to commons on the new upstream model (Phase 3) Move EventFinderFilterAssembler(+QueryState), the loaders (NoteEventLoader, FilterMissingEvents, FilterMissingAddressables, AddressableAuthorRelayLoader) and watchers (EventWatcher, FilterRepliesAndReactionsToNotes/Addresses) from amethyst/reqCommand/event/ into commons/relayClient/event/, mirroring the Phase 2 user-finder move. - EventFinderQueryState carries the narrow UserFinderAccount; DROPS AccountScopedQuery. Attribution stays inline via userFinderPubkeyHex; ExplainedFilter/SubPurpose tags (REFERENCED_EVENTS / ENGAGEMENT) preserved. - cache: LocalCache -> ICacheProvider threaded into NoteEventLoaderSubAssembler + the FilterMissing* functions (which called the LocalCache singleton statically); getOrCreateUser is now nullable at these sites. Added ICacheProvider.checkGetOrCreateUser default; LocalCache.checkGetOrCreateUser now overrides it. - SingleSubNoEoseCacheEoseManager moved to commons (account-agnostic accountPubKeyOf); its two amethyst callers keep attribution via a new amethyst subclass AccountScopedSingleSubNoEoseCacheEoseManager (ChannelLoader) or the plain commons base (NWCPaymentWatcher, which never attributed). Android unchanged via EventFinderShims.kt (typealiases + AccountViewModel overload); EventObservers.kt stays in amethyst. New commons EventFinderFilterAssemblerSubscription (LocalEventFinder + (note) overload reusing LocalUserFinderAccount) for Desktop. Updated the direct loader-function callers (search/hashtag/thread sub-assemblers + the moved test) to import from commons and pass LocalCache. Green: commons JVM + iOS purity, :amethyst compilePlayDebugKotlin, :desktopApp compile, spotless. Co-Authored-By: Claude Opus 4.8 --- .../amethyst/model/LocalCache.kt | 2 +- ...ntScopedSingleSubNoEoseCacheEoseManager.kt | 46 ++++++++++ .../ChannelLoaderSubAssembler.kt | 4 +- ...lerSubscription.kt => EventFinderShims.kt} | 40 ++++----- .../nwc/NWCPaymentWatcherSubAssembler.kt | 2 +- .../subassemblies/FilterByAddress.kt | 6 +- .../subassemblies/FilterByEvent.kt | 8 +- .../hashtag/datasource/FilterHashtagLabels.kt | 6 +- .../FilterMissingEventsForThread.kt | 17 ++-- ...ssableAuthorRelayLoaderSubAssemblerTest.kt | 5 +- .../commons/model/cache/ICacheProvider.kt | 10 +++ .../SingleSubNoEoseCacheEoseManager.kt | 13 ++- .../event/EventFinderFilterAssembler.kt | 25 +++--- .../EventFinderFilterAssemblerSubscription.kt | 84 +++++++++++++++++++ ...ddressableAuthorRelayLoaderSubAssembler.kt | 16 ++-- .../loaders/FilterMissingAddressables.kt | 34 ++++---- .../event/loaders/FilterMissingEvents.kt | 38 +++++---- .../loaders/NoteEventLoaderSubAssembler.kt | 17 ++-- .../watchers/EventWatcherSubAssembler.kt | 16 ++-- .../FilterRepliesAndReactionsToAddresses.kt | 6 +- .../FilterRepliesAndReactionsToNotes.kt | 6 +- 21 files changed, 280 insertions(+), 121 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/AccountScopedSingleSubNoEoseCacheEoseManager.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/{EventFinderFilterAssemblerSubscription.kt => EventFinderShims.kt} (61%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt (85%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/EventFinderFilterAssembler.kt (70%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssemblerSubscription.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/loaders/FilterMissingAddressables.kt (79%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/loaders/FilterMissingEvents.kt (79%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/loaders/NoteEventLoaderSubAssembler.kt (67%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/watchers/EventWatcherSubAssembler.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/watchers/FilterRepliesAndReactionsToAddresses.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/event/watchers/FilterRepliesAndReactionsToNotes.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index cd2963d6d7..4029469535 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -679,7 +679,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() } - fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull() + override fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull() fun load(keys: List): List = keys.mapNotNull(::checkGetOrCreateUser) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/AccountScopedSingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/AccountScopedSingleSubNoEoseCacheEoseManager.kt new file mode 100644 index 0000000000..c76800af32 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/AccountScopedSingleSubNoEoseCacheEoseManager.kt @@ -0,0 +1,46 @@ +/* + * 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.eoseManagers + +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager +import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient + +/** + * Amethyst variant of [SingleSubNoEoseCacheEoseManager] that restores single-account + * attribution for [AccountScopedQuery] keys. + * + * The commons base is account-agnostic (attribution defaults to null) so it can live in + * commonMain. Query states that carry an [Account] (home feed, channels, notifications, …) + * subclass this so their single-account REQs still show up attributed in "Active Relay + * Subscriptions". + * + * Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses + * HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the + * other under "not attributed" despite both being built from a single account's data. + */ +abstract class AccountScopedSingleSubNoEoseCacheEoseManager( + client: INostrClient, + allKeys: () -> Set, + invalidateAfterEose: Boolean = false, +) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose) { + override fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt index c14d14bd19..f06d9a28d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.AccountScopedSingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelLoaderSubAssembler( client: INostrClient, allKeys: () -> Set, -) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { +) : AccountScopedSingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List): List = filterMissingChannelsById(keys) override fun distinct(key: ChannelFinderQueryState) = key.channel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderShims.kt similarity index 61% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderShims.kt index 005f4f203e..b3a9458bab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderShims.kt @@ -21,30 +21,30 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription -import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +/** + * Back-compat aliases: the per-note event finder moved to commons + * (`com.vitorpamplona.amethyst.commons.relayClient.event`). Existing Android call + * sites that reference these by their old names resolve here. + */ +typealias EventFinderFilterAssembler = com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderFilterAssembler + +typealias EventFinderQueryState = com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState + +/** + * Android convenience overload: pulls the account + shared event-finder data source + * out of [accountViewModel] and delegates to the commons subscription. `Account` + * is-a `UserFinderAccount`, so no adaptation is needed. + */ @Composable fun EventFinderFilterAssemblerSubscription( note: Note, accountViewModel: AccountViewModel, -) = EventFinderFilterAssemblerSubscription(note, accountViewModel.account, accountViewModel.dataSources().eventFinder) - -@Composable -fun EventFinderFilterAssemblerSubscription( - note: Note, - account: Account, - dataSource: EventFinderFilterAssembler, -) { - // different screens get different states - // even if they are tracking the same tag. - val state = - remember(note, account) { - EventFinderQueryState(note, account) - } - - LifecycleAwareKeyDataSourceSubscription(state, dataSource) -} +) = EventFinderFilterAssemblerSubscription( + note, + accountViewModel.account, + accountViewModel.dataSources().eventFinder, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt index b179e5a042..51e4ae0e67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt index a527046727..df31d45479 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingAddressables +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindAddress import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress @@ -37,7 +37,7 @@ fun filterByAddress( val list = mapOfSet { if (note.event == null) { - potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindAddress(LocalCache, note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, note.address) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index 0a56940449..ae225e1b27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingEvents +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -38,7 +38,7 @@ fun filterByEvent( val list = mapOfSet { if (note !is AddressableNote && note.event == null) { - potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindEvent(LocalCache, note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, note.idHex) } } @@ -46,7 +46,7 @@ fun filterByEvent( // loads threading that is event-based note.replyTo?.forEach { parentNote -> if (parentNote !is AddressableNote && note.event == null) { - potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindEvent(LocalCache, note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, note.idHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt index 670eff235a..7106576dfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingEvents +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -93,7 +93,7 @@ fun filterHashtagLabels( val target = LocalCache.getNoteIfExists(targetId) if (target?.event == null) { val targetNote = LocalCache.getOrCreateNote(targetId) - potentialRelaysToFindEvent(targetNote).ifEmpty { relays }.forEach { relayUrl -> + potentialRelaysToFindEvent(LocalCache, targetNote).ifEmpty { relays }.forEach { relayUrl -> add(relayUrl, targetId) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index c2a7d7ba40..a9de4c769d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies import com.vitorpamplona.amethyst.commons.model.ThreadAssembler +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingAddressables +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingEvents +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindAddress +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet @@ -37,14 +38,14 @@ fun filterMissingEventsForThread( val missingEvents = mapOfSet { if (threadInfo.root.event == null && threadInfo.root !is AddressableNote) { - potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl -> + potentialRelaysToFindEvent(LocalCache, threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl -> add(relayUrl, threadInfo.root.idHex) } } threadInfo.allNotes.forEach { if (it !is AddressableNote && it.event == null) { - potentialRelaysToFindEvent(it).ifEmpty { defaultRelays }.forEach { relayUrl -> + potentialRelaysToFindEvent(LocalCache, it).ifEmpty { defaultRelays }.forEach { relayUrl -> add(relayUrl, it.idHex) } } @@ -59,14 +60,14 @@ fun filterMissingEventsForThread( // note's aTag idHex into the hex-keyed event-hint index, which throws // on the non-hex string and kills the whole filter build — leaving a // thread opened on an uncached naddr permanently unfetched. - potentialRelaysToFindAddress(rootNote).ifEmpty { defaultRelays }.forEach { relayUrl -> + potentialRelaysToFindAddress(LocalCache, rootNote).ifEmpty { defaultRelays }.forEach { relayUrl -> add(relayUrl, rootNote.address) } } threadInfo.allNotes.forEach { if (it is AddressableNote && it.event == null) { - potentialRelaysToFindAddress(it).ifEmpty { defaultRelays }.forEach { relayUrl -> + potentialRelaysToFindAddress(LocalCache, it).ifEmpty { defaultRelays }.forEach { relayUrl -> add(relayUrl, it.address) } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt index 842471e6a4..82fd16f813 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt @@ -20,11 +20,12 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.AddressableAuthorRelayLoaderSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.quartz.nip01Core.core.Address import io.mockk.every import io.mockk.mockk diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt index 3f2e59bd64..7d9091967f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -142,5 +142,15 @@ interface ICacheProvider { */ fun getOrCreateUser(pubkey: HexKey): User? + /** + * Gets or creates a User by public key hex, swallowing any failure. + * Used by the event-finder relay-hint scan, which touches many potentially + * malformed pubkeys and must never throw mid-scan. + * + * @param key The user's public key in hex format + * @return The User (existing or newly created), or null on failure + */ + fun checkGetOrCreateUser(key: HexKey): User? = runCatching { getOrCreateUser(key) }.getOrNull() + fun justConsumeMyOwnEvent(event: Event): Boolean } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index a80357e283..622d9f8fbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -18,11 +18,9 @@ * 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.eoseManagers +package com.vitorpamplona.amethyst.commons.relayClient.eoseManagers -import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.attributedTo -import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -85,9 +83,10 @@ abstract class SingleSubNoEoseCacheEoseManager( /** * The account behind [key], when the key is account-scoped. Null for keys about other users. * - * Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses - * HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the - * other under "not attributed" despite both being built from a single account's data. + * Account-agnostic in commons: front ends that want single-account attribution override this + * (see the amethyst `AccountScopedSingleSubNoEoseCacheEoseManager`, which reads + * `(key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex`). The default returns null, + * so pooled / cross-account subscriptions are filed as "not attributed". */ - private fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex + open fun accountPubKeyOf(key: Any?): String? = null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssembler.kt similarity index 70% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssembler.kt index f1f320ae50..a5e1ed022c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssembler.kt @@ -18,36 +18,35 @@ * 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 +package com.vitorpamplona.amethyst.commons.relayClient.event import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.AddressableAuthorRelayLoaderSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.AddressableAuthorRelayLoaderSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.NoteEventLoaderSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.event.watchers.EventWatcherSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderAccount +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag @Stable class EventFinderQueryState( val note: Note, - override val account: Account, -) : AccountScopedQuery + val account: UserFinderAccount, +) @Stable class EventFinderFilterAssembler( client: INostrClient, - cache: LocalCache, + cache: ICacheProvider, userFinder: UserFinderFilterAssembler, ) : ComposeSubscriptionManager() { val group = listOf( - NoteEventLoaderSubAssembler(client, ::allKeys), + NoteEventLoaderSubAssembler(client, cache, ::allKeys), EventWatcherSubAssembler(client, ::allKeys), AddressableAuthorRelayLoaderSubAssembler(cache, ::allKeys, userFinder), ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssemblerSubscription.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssemblerSubscription.kt new file mode 100644 index 0000000000..bf9f637ec1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/EventFinderFilterAssemblerSubscription.kt @@ -0,0 +1,84 @@ +/* + * 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.commons.relayClient.event + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccount +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderAccount + +/** + * The shared per-note event data source (reactions / zaps / reposts / replies / + * OTS / references) for the current front end. Provided once near the composition + * root (Android via AppModules, Desktop via its subscriptions coordinator). + * Reading it without a provider is a programming error — the per-note observers + * must never be reachable from a composition that has no relay client (e.g. the + * Android `:napplet` sandbox process). + * + * The *account* half is reused from the user-finder: [LocalUserFinderAccount] + * already carries the narrow relay-hint seam the event loaders need. + */ +val LocalEventFinder = + staticCompositionLocalOf { + error("LocalEventFinder not provided") + } + +/** + * Subscribes to relay updates for [note]'s interactions (reactions, zaps, + * reposts, replies, …) for as long as this composable is in composition, + * coalesced with every other on-screen note into batched REQs by [dataSource]. + * + * Like the user-finder, because a `LazyColumn` composes only the visible window + * (+ a small prefetch buffer) this means "load interactions only for notes + * currently on screen" — [LifecycleAwareKeyDataSourceSubscription] unsubscribes + * ~30s after the row leaves composition or the app is backgrounded. + */ +@Composable +fun EventFinderFilterAssemblerSubscription( + note: Note, + account: UserFinderAccount, + dataSource: EventFinderFilterAssembler, +) { + // Different screens get their own query-state instance even when tracking + // the same note; the assembler dedups to one REQ per note. + val state = + remember(note, account) { + EventFinderQueryState(note, account) + } + + LifecycleAwareKeyDataSourceSubscription(state, dataSource) +} + +/** + * Convenience overload that reads the front end's [LocalEventFinder] and + * [LocalUserFinderAccount] from the composition. + */ +@Composable +fun EventFinderFilterAssemblerSubscription(note: Note) { + EventFinderFilterAssemblerSubscription( + note = note, + account = LocalUserFinderAccount.current, + dataSource = LocalEventFinder.current, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt index 9dba6ed2bd..68e5a40544 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt @@ -18,15 +18,15 @@ * 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.loaders +package com.vitorpamplona.amethyst.commons.relayClient.event.loaders +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState import com.vitorpamplona.amethyst.commons.service.BundledUpdate -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -41,7 +41,7 @@ import kotlinx.coroutines.IO * relay list arrives, [EventFinderFilterAssembler] is invalidated and can query the correct relay. */ class AddressableAuthorRelayLoaderSubAssembler( - val cache: LocalCache, + val cache: ICacheProvider, val allKeys: () -> Set, val userFinder: UserFinderFilterAssembler, ) : IEoseManager { @@ -69,7 +69,7 @@ class AddressableAuthorRelayLoaderSubAssembler( val note = key.note if (note is AddressableNote && note.event == null) { val author = cache.getOrCreateUser(note.address.pubKeyHex) - if (author.authorRelayList() == null) { + if (author != null && author.authorRelayList() == null) { needed.add(UserFinderQueryState(author, key.account)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingAddressables.kt similarity index 79% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingAddressables.kt index 41eac22796..f52e1a3242 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingAddressables.kt @@ -18,33 +18,36 @@ * 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.loaders +package com.vitorpamplona.amethyst.commons.relayClient.event.loaders +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -fun potentialRelaysToFindAddress(note: AddressableNote): Set { +fun potentialRelaysToFindAddress( + cache: ICacheProvider, + note: AddressableNote, +): Set { val set = mutableSetOf() - LocalCache.getOrCreateUser(note.address.pubKeyHex).outboxRelays()?.let { + cache.getOrCreateUser(note.address.pubKeyHex)?.outboxRelays()?.let { set.addAll(it) } - set.addAll(LocalCache.relayHints.hintsForAddress(note.idHex)) + set.addAll(cache.relayHints.hintsForAddress(note.idHex)) - LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } note.replyTo?.forEach { parentNote -> set.addAll(parentNote.relays) - LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } parentNote.author?.inboxRelays()?.let { set.addAll(it) } } @@ -52,7 +55,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set note.replies.forEach { childNote -> set.addAll(childNote.relays) - LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } childNote.author?.outboxRelays()?.let { set.addAll(it) } } @@ -72,13 +75,16 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set return set } -fun filterMissingAddressables(keys: List): List { +fun filterMissingAddressables( + cache: ICacheProvider, + keys: List, +): List { val addressesPerRelay = mapOfSet { keys.forEach { key -> - val default = key.account.followPlusAllMineWithSearch.flow.value + val default = key.account.followPlusAllMineWithSearchRelays() if (key.note is AddressableNote && key.note.event == null) { - potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindAddress(cache, key.note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, key.note.address) } } @@ -86,7 +92,7 @@ fun filterMissingAddressables(keys: List): List if (note is AddressableNote && note.event == null) { - potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindAddress(cache, note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, note.address) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingEvents.kt similarity index 79% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingEvents.kt index f9f545fc00..65cac46200 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/FilterMissingEvents.kt @@ -18,33 +18,36 @@ * 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.loaders +package com.vitorpamplona.amethyst.commons.relayClient.event.loaders +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -fun potentialRelaysToFindEvent(note: Note): Set { +fun potentialRelaysToFindEvent( + cache: ICacheProvider, + note: Note, +): Set { val set = mutableSetOf() - set.addAll(LocalCache.relayHints.hintsForEvent(note.idHex)) + set.addAll(cache.relayHints.hintsForEvent(note.idHex)) note.author?.outboxRelays()?.let { set.addAll(it) } - LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } note.replyTo?.forEach { parentNote -> set.addAll(parentNote.relays) - LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } parentNote.author?.inboxRelays()?.let { set.addAll(it) } } @@ -52,7 +55,7 @@ fun potentialRelaysToFindEvent(note: Note): Set { note.replies.forEach { childNote -> set.addAll(childNote.relays) - LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } + cache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } childNote.author?.outboxRelays()?.let { set.addAll(it) } } @@ -81,7 +84,7 @@ fun potentialRelaysToFindEvent(note: Note): Set { val noteEvent = parent.event if (noteEvent is PubKeyHintProvider) { noteEvent.linkedPubKeys().forEach { potentialAuthor -> - LocalCache.checkGetOrCreateUser(potentialAuthor)?.let { potentialAuthor -> + cache.checkGetOrCreateUser(potentialAuthor)?.let { potentialAuthor -> potentialAuthor.outboxRelays()?.let { set.addAll(it) } potentialAuthor.inboxRelays()?.let { set.addAll(it) } } @@ -98,18 +101,21 @@ fun potentialRelaysToFindEvent(note: Note): Set { return set } -fun filterMissingEvents(keys: List): List { +fun filterMissingEvents( + cache: ICacheProvider, + keys: List, +): List { val eventsPerRelay = mapOfSet { keys.forEach { key -> - val default = key.account.followPlusAllMineWithSearch.flow.value + val default = key.account.followPlusAllMineWithSearchRelays() if (key.note !is AddressableNote && key.note.event == null) { - potentialRelaysToFindEvent(key.note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindEvent(cache, key.note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, key.note.idHex) } - key.account.searchRelayList.flow.value.forEach { relayUrl -> + key.account.searchRelays().forEach { relayUrl -> add(relayUrl, key.note.idHex) } } @@ -117,7 +123,7 @@ fun filterMissingEvents(keys: List): List if (note !is AddressableNote && note.event == null) { - potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + potentialRelaysToFindEvent(cache, note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, note.idHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/NoteEventLoaderSubAssembler.kt similarity index 67% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/NoteEventLoaderSubAssembler.kt index 9ff15e6407..172ba4d101 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/loaders/NoteEventLoaderSubAssembler.kt @@ -18,21 +18,28 @@ * 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.loaders +package com.vitorpamplona.amethyst.commons.relayClient.event.loaders -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient class NoteEventLoaderSubAssembler( client: INostrClient, + val cache: ICacheProvider, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List) = listOfNotNull( - filterMissingEvents(keys), - filterMissingAddressables(keys), + filterMissingEvents(cache, keys), + filterMissingAddressables(cache, keys), ).flatten() override fun distinct(key: EventFinderQueryState) = key.note + + // Attribute to the account that owns this subscription, when a single account is watching. + // Deduped by pubkey hex, not by account identity, so two objects for the same logged-in user + // don't look like two accounts and suppress attribution. + override fun accountPubKeyOf(key: Any?): String? = (key as? EventFinderQueryState)?.account?.userFinderPubkeyHex } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/EventWatcherSubAssembler.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/EventWatcherSubAssembler.kt index e58b596d2d..25a9bb3b5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/EventWatcherSubAssembler.kt @@ -18,15 +18,15 @@ * 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 +package com.vitorpamplona.amethyst.commons.relayClient.event.watchers +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast -import com.vitorpamplona.amethyst.service.relays.MutableTime -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState +import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.commons.relays.MutableTime +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -67,7 +67,7 @@ class EventWatcherSubAssembler( // the same logged-in user would look like two accounts and suppress attribution entirely. val soleAccountPubKey = keys - .mapTo(mutableSetOf()) { it.account.userProfile().pubkeyHex } + .mapTo(mutableSetOf()) { it.account.userFinderPubkeyHex } .singleOrNull() return groupByRelayPresence(lastNotesOnFilter, latestEOSEs) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt index cce973f915..16e0f9f435 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -18,12 +18,12 @@ * 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 +package com.vitorpamplona.amethyst.commons.relayClient.event.watchers +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt index 64510793f8..f96a5b46fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -20,12 +20,12 @@ */ @file:Suppress("DEPRECATION") -package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers +package com.vitorpamplona.amethyst.commons.relayClient.event.watchers +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent