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.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
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.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.DualCase
@@ -45,7 +49,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
@@ -63,6 +70,10 @@ class DesktopLocalCache : ICacheProvider {
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 {
const val MAX_NOTES = 50_000
const val MAX_USERS = 25_000
@@ -71,6 +82,25 @@ class DesktopLocalCache : ICacheProvider {
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 -----
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.
* Returns true if the event was consumed (new), false if already seen.
* Routes an event to the appropriate consume method via kind-based registry.
* O(1) dispatch. Returns true if the event was consumed (new), false if already seen.
*/
fun consume(
event: Event,
relay: NormalizedRelayUrl?,
): Boolean =
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
}
}
): Boolean = consumers[event.kind]?.invoke(event, relay) ?: false
/**
* Consumes a kind 1 text note event.
@@ -268,6 +272,71 @@ class DesktopLocalCache : ICacheProvider {
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 -----
/**
@@ -409,6 +478,7 @@ class DesktopLocalCache : ICacheProvider {
notes.clear()
addressableNotes.clear()
deletedEvents.clear()
_followedUsers.value = emptySet()
}
}