refactor(git): index PR updates via LocalCache.observeEvents

GitPullRequestUpdateIndex now subscribes to a kind-1619-filtered
LocalCache.observeEvents instead of LocalCache.live.newEventBundles. The
indexed observable seeds its matching set from the cache index (via init())
and re-emits the full list on each new PR update, so the manual onStart
full-cache scan and the per-bundle type filtering over every event of every
kind are both gone. The collector just reduces the list to the
latest-per-parent map. PR updates are rare, so recomputing the whole map per
emission is cheaper than scanning every bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
This commit is contained in:
Claude
2026-06-29 23:06:11 +00:00
parent 2316e72573
commit 0d8936e8d3
@@ -20,7 +20,9 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.model.LocalCache.observeEvents
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -28,17 +30,24 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
/**
* Cross-screen index of the most recent NIP-34 pull-request update event
* (kind 1619) per parent pull-request id, kept up to date from
* [LocalCache.live.newEventBundles]. A PR update *revises* its parent PR with a
* [LocalCache.observeEvents]. A PR update *revises* its parent PR with a
* newer commit / merge base, so the UI folds the latest one into the PR rather
* than listing updates separately. Like [GitStatusIndex], updates aren't tracked
* in `Note.replies`, so a per-row cache scan would otherwise be required.
*
* The kind-indexed [observeEvents] subscription replaces both the full-cache
* `onStart` scan and the per-bundle type filtering the old
* `LocalCache.live.newEventBundles` flow needed: the observable's `init()` seeds
* the matching set from the index and re-emits the whole list on every new 1619,
* so we just reduce it to the latest-per-parent map each time. PR updates are
* rare, so recomputing the full map per emission is cheaper than it scanning
* every event of every kind.
*/
object GitPullRequestUpdateIndex {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -50,35 +59,21 @@ object GitPullRequestUpdateIndex {
fun startIfNeeded() {
if (!started.compareAndSet(false, true)) return
scope.launch {
LocalCache.live.newEventBundles
.onStart {
val initial = HashMap<HexKey, GitPullRequestUpdateEvent>()
LocalCache.notes.forEach { _, note ->
val event = note.event as? GitPullRequestUpdateEvent ?: return@forEach
val target = event.parentPullRequestId() ?: return@forEach
val current = initial[target]
if (current == null || event.createdAt > current.createdAt) {
initial[target] = event
}
}
mutableLatestByPullRequest.value = initial
}.collect { bundle -> processBundle(bundle) }
LocalCache
.observeEvents<GitPullRequestUpdateEvent>(Filter(kinds = listOf(GitPullRequestUpdateEvent.KIND)))
.collect { events -> mutableLatestByPullRequest.value = latestByParent(events) }
}
}
private fun processBundle(bundle: Set<Note>) {
val snapshot = mutableLatestByPullRequest.value ?: emptyMap()
var modified: HashMap<HexKey, GitPullRequestUpdateEvent>? = null
for (note in bundle) {
val event = note.event as? GitPullRequestUpdateEvent ?: continue
private fun latestByParent(events: List<GitPullRequestUpdateEvent>): Map<HexKey, GitPullRequestUpdateEvent> {
val latest = HashMap<HexKey, GitPullRequestUpdateEvent>()
for (event in events) {
val target = event.parentPullRequestId() ?: continue
val map = modified ?: snapshot
val current = map[target]
val current = latest[target]
if (current == null || event.createdAt > current.createdAt) {
if (modified == null) modified = HashMap(snapshot)
modified[target] = event
latest[target] = event
}
}
modified?.let { mutableLatestByPullRequest.value = it }
return latest
}
}