feat(cache): kind-based consumer registry + new event types

Replace when-dispatch with HashMap<Int, Handler> for O(1) event routing.
Add consume methods for: RepostEvent (kind 6), ContactListEvent (kind 3),
LongTextNoteEvent (kind 30023), BookmarkListEvent (kind 30001).
Replace @Volatile followedUsers with MutableStateFlow for thread safety
and Compose observability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-03-23 08:26:45 +02:00
parent e834dbd1e5
commit 684ba158ab
@@ -33,11 +33,15 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.DualCase
@@ -45,7 +49,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -63,6 +70,10 @@ class DesktopLocalCache : ICacheProvider {
val eventStream = DesktopCacheEventStream() val eventStream = DesktopCacheEventStream()
/** Cached follow set for the logged-in user. Thread-safe + Compose-observable. */
private val _followedUsers = MutableStateFlow<Set<HexKey>>(emptySet())
val followedUsers: StateFlow<Set<HexKey>> = _followedUsers.asStateFlow()
companion object { companion object {
const val MAX_NOTES = 50_000 const val MAX_NOTES = 50_000
const val MAX_USERS = 25_000 const val MAX_USERS = 25_000
@@ -71,6 +82,25 @@ class DesktopLocalCache : ICacheProvider {
val paymentTracker = NwcPaymentTracker() val paymentTracker = NwcPaymentTracker()
// ----- Kind-based event consumer registry -----
private val consumers = HashMap<Int, (Event, NormalizedRelayUrl?) -> Boolean>()
init {
consumers[MetadataEvent.KIND] = { e, _ ->
consumeMetadata(e as MetadataEvent)
true
}
consumers[TextNoteEvent.KIND] = { e, r -> consumeTextNote(e as TextNoteEvent, r) }
consumers[ReactionEvent.KIND] = { e, r -> consumeReaction(e as ReactionEvent, r) }
consumers[LnZapRequestEvent.KIND] = { e, r -> consumeZapRequest(e as LnZapRequestEvent, r) }
consumers[LnZapEvent.KIND] = { e, r -> consumeZap(e as LnZapEvent, r) }
consumers[RepostEvent.KIND] = { e, r -> consumeRepost(e as RepostEvent, r) }
consumers[ContactListEvent.KIND] = { e, _ -> consumeContactList(e as ContactListEvent) }
consumers[LongTextNoteEvent.KIND] = { e, r -> consumeLongTextNote(e as LongTextNoteEvent, r) }
consumers[BookmarkListEvent.KIND] = { e, _ -> consumeBookmarkList(e as BookmarkListEvent) }
}
// ----- User operations ----- // ----- User operations -----
override fun getUserIfExists(pubkey: HexKey): User? = users.get(pubkey) override fun getUserIfExists(pubkey: HexKey): User? = users.get(pubkey)
@@ -140,42 +170,16 @@ class DesktopLocalCache : ICacheProvider {
} }
} }
// ----- Event consumption (mirrors Android LocalCache pattern) ----- // ----- Event consumption (kind-based registry) -----
/** /**
* Routes an event to the appropriate consume method. * Routes an event to the appropriate consume method via kind-based registry.
* Returns true if the event was consumed (new), false if already seen. * O(1) dispatch. Returns true if the event was consumed (new), false if already seen.
*/ */
fun consume( fun consume(
event: Event, event: Event,
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
): Boolean = ): Boolean = consumers[event.kind]?.invoke(event, relay) ?: false
when (event) {
is MetadataEvent -> {
consumeMetadata(event)
true
}
is TextNoteEvent -> {
consumeTextNote(event, relay)
}
is ReactionEvent -> {
consumeReaction(event, relay)
}
is LnZapRequestEvent -> {
consumeZapRequest(event, relay)
}
is LnZapEvent -> {
consumeZap(event, relay)
}
else -> {
false
}
}
/** /**
* Consumes a kind 1 text note event. * Consumes a kind 1 text note event.
@@ -268,6 +272,71 @@ class DesktopLocalCache : ICacheProvider {
return true return true
} }
/**
* Consumes a kind 6 repost event.
* Links repost to target note via e-tag.
*/
private fun consumeRepost(
event: RepostEvent,
relay: NormalizedRelayUrl?,
): Boolean {
val note = getOrCreateNote(event.id)
if (note.event != null) return false
val author = getOrCreateUser(event.pubKey)
val boostedNote = event.boostedEventId()?.let { getNoteIfExists(it) }
val repliesTo = listOfNotNull(boostedNote)
note.loadEvent(event, author, repliesTo)
relay?.let { note.addRelay(it) }
boostedNote?.addBoost(note)
return true
}
/**
* Consumes a kind 3 contact list event (replaceable).
* Updates the cached followedUsers set.
*/
private fun consumeContactList(event: ContactListEvent): Boolean {
val currentFollows = _followedUsers.value
val newFollows = event.verifiedFollowKeySet()
if (newFollows != currentFollows) {
_followedUsers.value = newFollows
}
return true
}
/**
* Consumes a kind 30023 long-form text note event.
* Creates Note in cache like TextNoteEvent.
*/
private fun consumeLongTextNote(
event: LongTextNoteEvent,
relay: NormalizedRelayUrl?,
): Boolean {
val note = getOrCreateNote(event.id)
if (note.event != null) return false
val author = getOrCreateUser(event.pubKey)
note.loadEvent(event, author, emptyList())
relay?.let { note.addRelay(it) }
return true
}
/**
* Consumes a kind 30001 bookmark list event (addressable/replaceable).
* Stores in addressableNotes cache.
*/
private fun consumeBookmarkList(event: BookmarkListEvent): Boolean {
val address = event.address()
val addressableNote = getOrCreateAddressableNote(address)
val author = getOrCreateUser(event.pubKey)
// Only update if newer
val existingEvent = addressableNote.event
if (existingEvent != null && existingEvent.createdAt >= event.createdAt) return false
addressableNote.loadEvent(event, author, emptyList())
return true
}
// ----- NWC Payment operations ----- // ----- NWC Payment operations -----
/** /**
@@ -409,6 +478,7 @@ class DesktopLocalCache : ICacheProvider {
notes.clear() notes.clear()
addressableNotes.clear() addressableNotes.clear()
deletedEvents.clear() deletedEvents.clear()
_followedUsers.value = emptySet()
} }
} }