Moves java.util.SortedSet to an arrayList.

This commit is contained in:
Vitor Pamplona
2026-04-30 15:57:26 -04:00
parent 728a396ff2
commit 12ed2af028
3 changed files with 130 additions and 21 deletions
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.utils.SortedList
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -39,23 +40,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.yield
/**
* Lifecycle state of an [ObservableEventStore.project] flow.
*
* - [Loading] is the initial state before the seed query completes.
* The UI should show a spinner / skeleton here.
* - [Loaded] holds the current deduped, sorted list of slots. Every
* membership change publishes a fresh [Loaded] instance; in-place
* addressable updates do *not* publish (the slots' own flows do).
*/
sealed interface ProjectionState<out T : Event> {
data object Loading : ProjectionState<Nothing>
data class Loaded<T : Event>(
val items: List<MutableStateFlow<T>>,
) : ProjectionState<T>
}
/**
* State machine that maintains a reactive view over an
* [ObservableEventStore] for a fixed set of [filters]. Pure logic, no
@@ -106,8 +90,8 @@ class EventStoreProjection<T : Event>(
* least one of these sets. Identity-keyed — [Filter] is `@Stable`
* but not a data class.
*/
private val perFilter: Map<Filter, java.util.SortedSet<Slot<T>>> =
filters.associateWith { sortedSetOf(slotComparator()) }
private val perFilter: Map<Filter, SortedList<Slot<T>>> =
filters.associateWith { SortedList(slotComparator<T>()) }
/**
* Run the seed query against the store and populate the indexes.
@@ -158,7 +142,7 @@ class EventStoreProjection<T : Event>(
if (perFilter.size == 1) {
return ProjectionState.Loaded(perFilter.values.first().map { it.flow })
}
val union = sortedSetOf(slotComparator<T>())
val union = SortedList(slotComparator<T>())
for (set in perFilter.values) union.addAll(set)
return ProjectionState.Loaded(union.map { it.flow })
}
@@ -260,7 +244,7 @@ class EventStoreProjection<T : Event>(
private fun admit(
slot: Slot<T>,
f: Filter,
set: java.util.SortedSet<Slot<T>>,
set: SortedList<Slot<T>>,
): Boolean {
set.add(slot)
val cap = f.limit ?: return false
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.cache.projection
import com.vitorpamplona.quartz.nip01Core.core.Event
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
/**
* Lifecycle state of an [ObservableEventStore.project] flow.
*
* - [Loading] is the initial state before the seed query completes.
* The UI should show a spinner / skeleton here.
* - [Loaded] holds the current deduped, sorted list of slots. Every
* membership change publishes a fresh [Loaded] instance; in-place
* addressable updates do *not* publish (the slots' own flows do).
*/
sealed interface ProjectionState<out T : Event> {
data object Loading : ProjectionState<Nothing>
data class Loaded<T : Event>(
val items: List<MutableStateFlow<T>>,
) : ProjectionState<T>
}
inline fun <T : Event> ProjectionState<T>.filterItems(predicate: (MutableStateFlow<T>) -> Boolean): ProjectionState<T> =
when (this) {
is ProjectionState.Loading -> this
is ProjectionState.Loaded -> ProjectionState.Loaded(items.filter(predicate))
}
inline fun <T : Event, R : Event> ProjectionState<T>.mapItems(transform: (MutableStateFlow<T>) -> MutableStateFlow<R>): ProjectionState<R> =
when (this) {
is ProjectionState.Loading -> ProjectionState.Loading
is ProjectionState.Loaded -> ProjectionState.Loaded(items.map(transform))
}
inline fun <T : Event> Flow<ProjectionState<T>>.filterItems(crossinline predicate: (MutableStateFlow<T>) -> Boolean): Flow<ProjectionState<T>> = map { it.filterItems(predicate) }
inline fun <T : Event> Flow<ProjectionState<T>>.mapItems(crossinline transform: (MutableStateFlow<T>) -> MutableStateFlow<T>): Flow<ProjectionState<T>> = map { it.mapItems(transform) }
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
/**
* KMP substitute for `java.util.TreeSet`. Backed by a sorted [ArrayList];
* O(log n) add/remove/contains via binary search, O(1) [last]/[size],
* in-order iteration.
*
* The [cmp] must be total — distinct elements must not compare equal,
* otherwise duplicates collapse silently. Optimised for small-to-medium
* `n`; insert/remove in the middle is O(n) due to ArrayList shifting,
* which is faster in practice than a tree for the bounded sizes typical
* of per-filter caps.
*/
class SortedList<E>(
private val cmp: Comparator<in E>,
) : Iterable<E> {
private val list = ArrayList<E>()
val size: Int get() = list.size
fun isEmpty(): Boolean = list.isEmpty()
fun add(element: E): Boolean {
val idx = list.binarySearch(element, cmp)
if (idx >= 0) return false
list.add(-idx - 1, element)
return true
}
fun remove(element: E): Boolean {
val idx = list.binarySearch(element, cmp)
if (idx < 0) return false
list.removeAt(idx)
return true
}
fun contains(element: E): Boolean = list.binarySearch(element, cmp) >= 0
fun last(): E = list.last()
fun addAll(other: Iterable<E>) {
for (e in other) add(e)
}
override fun iterator(): Iterator<E> = list.iterator()
}