refactor(commons): EventCache.filter returns a List, not a SortedSet

java.util.SortedSet has no common equivalent, and it was in the cache's own
signature plus both *ListMatchingFilter observables. The ripple was smaller
than the plan assumed: ICacheProvider never declared filter and DesktopLocalCache
has no equivalent, so the type was EventCache-local.

The result is now a List carrying the same order the SortedSet did —
CreatedAtIdHexComparator, newest first. That order is load-bearing rather than
incidental: the napplet gateway answers REQs out of this call, and NIP-01 has
relays return events newest first.

The comparator was also doing something the type change had to preserve. It
defines uniqueness by reference, so the sorted set collapsed duplicate
references — which a filter repeating a kind produces, since the addressable
scan runs once per kind. toSet() before sorting keeps exactly that: Note
declares no equals(), so a plain Set de-duplicates by reference too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8
This commit is contained in:
Claude
2026-09-21 15:27:48 +00:00
parent 624ac88eee
commit e6c10fca31
5 changed files with 14 additions and 13 deletions
@@ -422,7 +422,6 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.util.SortedSet
/**
* The in-memory event store: every `Note`, `User` and `Channel` the app has consumed, plus the
@@ -590,7 +589,7 @@ open class EventCache :
}
}
fun filter(filter: Filter): SortedSet<Note> = filter(filter) { true }
fun filter(filter: Filter): List<Note> = filter(filter) { true }
/**
* Every note matching [filter]'s NIP-01 fields that also satisfies [predicate].
@@ -603,7 +602,7 @@ open class EventCache :
fun filter(
filter: Filter,
predicate: (Note) -> Boolean,
): SortedSet<Note> {
): List<Note> {
val byKinds = filter.kinds?.filter { it.isAddressable() || it.isReplaceable() }
val addressableMatches =
@@ -642,14 +641,20 @@ open class EventCache :
}
}
val all = (addressableMatches + noteMatches).toSortedSet(CreatedAtIdHexComparator)
// toSet() before sorting: a filter that repeats a kind scans that kind twice, and the
// SortedSet this used to return collapsed the repeats. Note declares no equals(), so a
// plain Set is the same reference-identity de-duplication that comparator gave.
//
// The order is the comparator's, newest first, and it is load-bearing: the napplet
// gateway answers REQs out of this, and NIP-01 has relays return events newest first.
val all = (addressableMatches + noteMatches).toSet().sortedWith(CreatedAtIdHexComparator)
val limit = filter.limit ?: return all
// Sorted first, then cut. Both halves arrive in hash-walk order, so taking before sorting
// dropped whichever matches the walk happened to reach last — the newest ones as often as
// not — and a query with 200 addressable matches never showed a single regular note.
if (all.size <= limit) return all
return all.asSequence().take(limit).toCollection(sortedSetOf(CreatedAtIdHexComparator))
return all.take(limit)
}
fun observeNotes(filter: Filter): Flow<List<Note>> =
@@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentSkipListSet
@@ -69,7 +68,7 @@ import java.util.concurrent.ConcurrentSkipListSet
*/
class EventListMatchingFilter<T : Event>(
private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val atOnce: (filter: Filter) -> List<Note>,
private val update: (List<T>) -> Unit,
) : Observable {
/** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */
@@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentSkipListSet
@@ -68,7 +67,7 @@ import java.util.concurrent.ConcurrentSkipListSet
*/
class NoteListMatchingFilter(
private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val atOnce: (filter: Filter) -> List<Note>,
private val update: (List<Note>) -> Unit,
) : Observable {
/** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */
@@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.utils.EventFactory
import java.util.TreeSet
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
import kotlin.concurrent.thread
@@ -65,7 +64,7 @@ class EventListMatchingFilterTest {
sink: (List<Event>) -> Unit,
) = EventListMatchingFilter<Event>(
filter = withFilter,
atOnce = { TreeSet(CreatedAtIdHexComparator) },
atOnce = { emptyList() },
update = sink,
)
@@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.utils.EventFactory
import java.util.TreeSet
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
import kotlin.concurrent.thread
@@ -67,7 +66,7 @@ class NoteListMatchingFilterTest {
sink: (List<Note>) -> Unit,
) = NoteListMatchingFilter(
filter = withFilter,
atOnce = { TreeSet(CreatedAtIdHexComparator) },
atOnce = { emptyList() },
update = sink,
)