mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(commons): audit follow-ups for the shared relayClient layer
Review findings on the three migration commits, all verified: - The shared feed-floor watcher sampled at 5 s for every feed, but the video swipe feed used 1 s before the refactor, so paging older videos waited up to five times longer. The interval is now a `floorSampleMs` override and Video keeps its 1 s cadence. - `endSub` cancelled the watcher jobs but never dropped the `userJobMap` entry, leaving a cancelled job list per account ever subscribed on every app-lifetime assembler. It now removes the entry. - `DesktopLocalCache.consume(nip19)` was a copy of the Android body. The NIP-19 seeding is now one default on `ICacheProvider`, built only from interface members, with a single `consumeEmbedded(event)` hook for the `nembed` branch (Android verifies via `justConsume`, Desktop via its `consume(event, relay)`). The default uses `checkGetOrCreateNote`, so an invalid id yields no placeholder instead of an exception. - `allRelayGroupChannels()` dumped the group cache through a predicate that always returned true; it now snapshots `values()`. - The music/podcast kind lists go back to `internal`: their only callers moved into commons with the dispatchers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J836W9ZjSUJUQ1d23TpiJo
This commit is contained in:
@@ -221,15 +221,6 @@ import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip19Bech32.isATag
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
@@ -745,7 +736,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
/** Every relay group we know of that is hosted on [relay] (its channel directory). */
|
||||
override fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl): List<RelayGroupChannel> = relayGroupChannels.filter { key, _ -> key.relayUrl == relay }
|
||||
|
||||
override fun allRelayGroupChannels(): List<RelayGroupChannel> = relayGroupChannels.filter { _, _ -> true }
|
||||
override fun allRelayGroupChannels(): List<RelayGroupChannel> = relayGroupChannels.values().toList()
|
||||
|
||||
/**
|
||||
* The [RelayGroupChannel] a group-scoped content [note] belongs to, resolved the same way
|
||||
@@ -3302,53 +3293,8 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
wasVerified: Boolean,
|
||||
): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
override fun consume(nip19: Entity) {
|
||||
when (nip19) {
|
||||
is NSec -> {
|
||||
getOrCreateUser(nip19.toPubKeyHex())
|
||||
}
|
||||
|
||||
is NPub -> {
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
|
||||
is NProfile -> {
|
||||
nip19.relay.forEach { relayHint ->
|
||||
relayHints.addKey(nip19.hex, relayHint)
|
||||
}
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
|
||||
is NNote -> {
|
||||
getOrCreateNote(nip19.hex)
|
||||
}
|
||||
|
||||
is NEvent -> {
|
||||
nip19.relay.forEach { relayHint ->
|
||||
relayHints.addEvent(nip19.hex, relayHint)
|
||||
}
|
||||
val note = getOrCreateNote(nip19.hex)
|
||||
if (note.author == null) {
|
||||
nip19.author?.let { note.author = checkGetOrCreateUser(it) }
|
||||
}
|
||||
}
|
||||
|
||||
is NEmbed -> {
|
||||
justConsume(nip19.event, null, false)
|
||||
}
|
||||
|
||||
is NRelay -> {}
|
||||
|
||||
is NAddress -> {
|
||||
val aTag = nip19.aTag()
|
||||
nip19.relay.forEach { relayHint ->
|
||||
relayHints.addAddress(aTag, relayHint)
|
||||
}
|
||||
getOrCreateAddressableNote(nip19.address())
|
||||
}
|
||||
|
||||
else -> { }
|
||||
}
|
||||
override fun consumeEmbedded(event: Event) {
|
||||
justConsume(event, null, false)
|
||||
}
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true)
|
||||
|
||||
Vendored
+38
-1
@@ -31,6 +31,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
@@ -176,7 +183,37 @@ interface ICacheProvider {
|
||||
* placeholder User / Note / AddressableNote, so a REQ built for it has something to
|
||||
* attach to. Used by the search sub-assemblers when the query is an npub/nevent/naddr.
|
||||
*/
|
||||
fun consume(nip19: Entity)
|
||||
fun consume(nip19: Entity) {
|
||||
when (nip19) {
|
||||
is NSec -> getOrCreateUser(nip19.toPubKeyHex())
|
||||
is NPub -> getOrCreateUser(nip19.hex)
|
||||
is NProfile -> {
|
||||
nip19.relay.forEach { relayHints.addKey(nip19.hex, it) }
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
is NNote -> checkGetOrCreateNote(nip19.hex)
|
||||
is NEvent -> {
|
||||
nip19.relay.forEach { relayHints.addEvent(nip19.hex, it) }
|
||||
val note = checkGetOrCreateNote(nip19.hex)
|
||||
if (note != null && note.author == null) {
|
||||
nip19.author?.let { note.author = checkGetOrCreateUser(it) }
|
||||
}
|
||||
}
|
||||
is NEmbed -> consumeEmbedded(nip19.event)
|
||||
is NAddress -> {
|
||||
val aTag = nip19.aTag()
|
||||
nip19.relay.forEach { relayHints.addAddress(aTag, it) }
|
||||
getOrCreateAddressableNote(nip19.address())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests an event carried inline in an `nembed`. Unlike [justConsumeMyOwnEvent] the event
|
||||
* did not come from this user, so implementations verify its signature like a relay event.
|
||||
*/
|
||||
fun consumeEmbedded(event: Event)
|
||||
|
||||
/**
|
||||
* Every NIP-29 relay group (kind 39000 metadata + rosters) this cache holds. The discovery
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
// also fan out as separate subscriptions — keeping them split avoids cross-kind over-fetching
|
||||
// and lets `makeMusicTracksFilter` / `makeMusicPlaylistsFilter` reuse the same parameterized
|
||||
// `filterMusicEventsByX` helpers below.
|
||||
val MUSIC_TRACK_KINDS = listOf(MusicTrackEvent.KIND)
|
||||
val MUSIC_PLAYLIST_KINDS = listOf(MusicPlaylistEvent.KIND)
|
||||
internal val MUSIC_TRACK_KINDS = listOf(MusicTrackEvent.KIND)
|
||||
internal val MUSIC_PLAYLIST_KINDS = listOf(MusicPlaylistEvent.KIND)
|
||||
|
||||
fun filterMusicEventsByAuthors(
|
||||
relay: NormalizedRelayUrl,
|
||||
|
||||
+4
-4
@@ -39,15 +39,15 @@ import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.Podcasting20PodcastMe
|
||||
// Episodes span both podcast drafts: NIP-F4 kind 54 (podcast-is-a-keypair) and Podcasting-2.0
|
||||
// kind 30054 (creator-is-a-keypair). Both are authored by the followed pubkey, so the same
|
||||
// author-scoped REQ pulls them into one merged feed.
|
||||
val PODCAST_EPISODE_KINDS = listOf(PodcastEpisodeEvent.KIND, Podcasting20EpisodeEvent.KIND)
|
||||
val PODCAST_KINDS = listOf(PodcastMetadataEvent.KIND)
|
||||
internal val PODCAST_EPISODE_KINDS = listOf(PodcastEpisodeEvent.KIND, Podcasting20EpisodeEvent.KIND)
|
||||
internal val PODCAST_KINDS = listOf(PodcastMetadataEvent.KIND)
|
||||
|
||||
// Podcasting-2.0 stores show metadata as a kind:30078 NIP-78 app-data event. That kind is
|
||||
// heavily overloaded, so this REQ MUST be constrained by `#d=["podcast-metadata"]` (see
|
||||
// [PODCAST_METADATA_D_FILTER]) or it would pull every client's app-data. Kept separate from
|
||||
// [PODCAST_KINDS] because the NIP-F4 kind:10154 metadata must NOT carry the `#d` constraint.
|
||||
val PODCASTING20_METADATA_KINDS = listOf(AppSpecificDataEvent.KIND)
|
||||
val PODCAST_METADATA_D_FILTER = mapOf("d" to listOf(Podcasting20PodcastMetadata.PODCAST_METADATA_D_TAG))
|
||||
internal val PODCASTING20_METADATA_KINDS = listOf(AppSpecificDataEvent.KIND)
|
||||
internal val PODCAST_METADATA_D_FILTER = mapOf("d" to listOf(Podcasting20PodcastMetadata.PODCAST_METADATA_D_TAG))
|
||||
|
||||
/**
|
||||
* Merges two relay-filter tag maps, unioning the value lists per key. Used to layer an extra
|
||||
|
||||
+6
-2
@@ -63,6 +63,9 @@ abstract class TopNavFeedSubAssembler<K : TopNavFeedQueryState>(
|
||||
/** Runs when the key's top-nav selection changes, right before the filters are re-issued. */
|
||||
protected open fun onListChanged(key: K) {}
|
||||
|
||||
/** How often (ms) a moving feed floor may re-issue the REQ; the video swipe feed wants a tighter cadence. */
|
||||
protected open val floorSampleMs: Long = 5000
|
||||
|
||||
/** The feed floors that re-issue the REQ (sampled) when they move. Defaults to every feed on the key. */
|
||||
protected open fun floors(key: K): List<StateFlow<Long?>> = key.feeds.map { it.lastNoteCreatedAtWhenFullyLoaded }
|
||||
|
||||
@@ -96,7 +99,7 @@ abstract class TopNavFeedSubAssembler<K : TopNavFeedQueryState>(
|
||||
if (floors.isNotEmpty()) {
|
||||
add(
|
||||
key.scope.launch(Dispatchers.IO) {
|
||||
combine(floors) { }.sample(5000).collectLatest {
|
||||
combine(floors) { }.sample(floorSampleMs).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
@@ -121,7 +124,7 @@ abstract class TopNavFeedSubAssembler<K : TopNavFeedQueryState>(
|
||||
subId: String,
|
||||
) {
|
||||
super.endSub(key, subId)
|
||||
userJobMap[key]?.forEach { it.cancel() }
|
||||
userJobMap.remove(key)?.forEach { it.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +142,7 @@ open class SingleTopNavFeedSubAssembler(
|
||||
allKeys: () -> Set<TopNavFeedQueryState>,
|
||||
private val makeFilter: TopNavFeedFilterMaker,
|
||||
private val resetEoseOnListChange: Boolean = false,
|
||||
override val floorSampleMs: Long = 5000,
|
||||
) : TopNavFeedSubAssembler<TopNavFeedQueryState>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: TopNavFeedQueryState,
|
||||
|
||||
+1
-1
@@ -27,4 +27,4 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
class VideoFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : TopNavFeedFilterAssembler<TopNavFeedQueryState>({ keys -> listOf(SingleTopNavFeedSubAssembler(client, keys, ::makePictureAndVideoFilter)) })
|
||||
) : TopNavFeedFilterAssembler<TopNavFeedQueryState>({ keys -> listOf(SingleTopNavFeedSubAssembler(client, keys, ::makePictureAndVideoFilter, floorSampleMs = 1000)) })
|
||||
|
||||
+1
-2
@@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
@@ -193,7 +192,7 @@ class ThreadAssemblerTest {
|
||||
|
||||
override fun getOrCreateUser(pubkey: HexKey): User? = null
|
||||
|
||||
override fun consume(nip19: Entity) = Unit
|
||||
override fun consumeEmbedded(event: Event) = Unit
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event): Boolean = false
|
||||
}
|
||||
|
||||
+1
-2
@@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -102,7 +101,7 @@ class ConcordChannelListLeaveTest {
|
||||
|
||||
override fun getOrCreateUser(pubkey: HexKey): User? = null
|
||||
|
||||
override fun consume(nip19: Entity) = Unit
|
||||
override fun consumeEmbedded(event: Event) = Unit
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event): Boolean = false
|
||||
}
|
||||
|
||||
+1
-2
@@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -100,7 +99,7 @@ class ConcordListLateArrivalTest {
|
||||
|
||||
override fun getOrCreateUser(pubkey: HexKey): User? = null
|
||||
|
||||
override fun consume(nip19: Entity) = Unit
|
||||
override fun consumeEmbedded(event: Event) = Unit
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event): Boolean = false
|
||||
}
|
||||
|
||||
+1
-2
@@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
|
||||
/**
|
||||
* Minimal [ICacheProvider] for the pure reply-resolution tests in this package: a fixed set of
|
||||
@@ -69,7 +68,7 @@ internal class StubCache(
|
||||
|
||||
override fun getOrCreateUser(pubkey: HexKey): User? = users[pubkey]
|
||||
|
||||
override fun consume(nip19: Entity) = Unit
|
||||
override fun consumeEmbedded(event: Event) = Unit
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event): Boolean = false
|
||||
}
|
||||
|
||||
+1
-2
@@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
@@ -156,7 +155,7 @@ class EventFinderFilterAssemblyTest {
|
||||
|
||||
override fun getOrCreateUser(pubkey: HexKey): User? = null
|
||||
|
||||
override fun consume(nip19: Entity) = Unit
|
||||
override fun consumeEmbedded(event: Event) = Unit
|
||||
|
||||
override fun justConsumeMyOwnEvent(event: Event): Boolean = false
|
||||
}
|
||||
|
||||
Vendored
+2
-33
@@ -50,14 +50,6 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QAddressableTag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuotes
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
@@ -1037,31 +1029,8 @@ class DesktopLocalCache : ICacheProvider {
|
||||
AddressableNote(address)
|
||||
}
|
||||
|
||||
/** Same seeding as the Android cache: relay hints + a placeholder for whatever the entity names. */
|
||||
override fun consume(nip19: Entity) {
|
||||
when (nip19) {
|
||||
is NSec -> getOrCreateUser(nip19.toPubKeyHex())
|
||||
is NPub -> getOrCreateUser(nip19.hex)
|
||||
is NProfile -> {
|
||||
nip19.relay.forEach { relayHints.addKey(nip19.hex, it) }
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
is NNote -> getOrCreateNote(nip19.hex)
|
||||
is NEvent -> {
|
||||
nip19.relay.forEach { relayHints.addEvent(nip19.hex, it) }
|
||||
val note = getOrCreateNote(nip19.hex)
|
||||
if (note.author == null) {
|
||||
nip19.author?.let { note.author = checkGetOrCreateUser(it) }
|
||||
}
|
||||
}
|
||||
is NEmbed -> consume(nip19.event, null)
|
||||
is NAddress -> {
|
||||
val aTag = nip19.aTag()
|
||||
nip19.relay.forEach { relayHints.addAddress(aTag, it) }
|
||||
getOrCreateAddressableNote(nip19.address())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
override fun consumeEmbedded(event: Event) {
|
||||
consume(event, null)
|
||||
}
|
||||
|
||||
// ----- Channel operations -----
|
||||
|
||||
Reference in New Issue
Block a user