Moves the new observables to commons

This commit is contained in:
Vitor Pamplona
2026-02-07 18:20:50 -05:00
parent 7813d20f8e
commit 571fb6af4b
7 changed files with 56 additions and 31 deletions
@@ -0,0 +1,90 @@
/**
* 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.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
val CreatedAtIdHexComparator: Comparator<Note> =
Comparator { note1, note2 ->
// Sort by created_at and idhex, but define uniqueness by reference
if (note1 === note2) return@Comparator 0
if (note1 == null) return@Comparator 1 // null is greater, moves to last
if (note2 == null) return@Comparator -1 // null is greater, moves to last
val createdAt1 = note1.createdAt()
val createdAt2 = note2.createdAt()
if (createdAt1 == createdAt2) {
val event1 = note1.event
val event2 = note2.event
if (event1 == null && event2 == null) return@Comparator 0
if (event1 == null) return@Comparator 1 // null is greater, moves to last
if (event2 == null) return@Comparator -1 // null is greater, moves to last
event1.id.compareTo(event2.id)
} else {
// Descending, inverts
compareValues(createdAt2, createdAt1)
}
}
object CreatedAtComparator : Comparator<Note> {
override fun compare(
first: Note?,
second: Note?,
): Int {
val firstEvent = first?.event
val secondEvent = second?.event
return if (firstEvent == null && secondEvent == null) {
0
} else if (firstEvent == null) {
1
} else if (secondEvent == null) {
-1
} else {
firstEvent.createdAt.compareTo(secondEvent.createdAt)
}
}
}
object CreatedAtComparatorAddresses : Comparator<AddressableNote> {
override fun compare(
first: AddressableNote?,
second: AddressableNote?,
): Int {
val firstEvent = first?.event
val secondEvent = second?.event
return if (firstEvent == null && secondEvent == null) {
0
} else if (firstEvent == null) {
1
} else if (secondEvent == null) {
-1
} else {
firstEvent.createdAt.compareTo(secondEvent.createdAt)
}
}
}
@@ -0,0 +1,68 @@
/**
* 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.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentSkipListSet
import kotlin.collections.mapNotNull
/**
* Creates a list of events (regular and addressable)
* that is updated every time a new event that matches
* the filter is received, including addressables.
*/
class EventListMatchingFilter(
private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val update: (List<Event>) -> Unit,
) : Observable {
// Keeping this here blocks it from being cleared from memory
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
override fun new(
event: Event,
note: Note,
) {
if (filter.match(event)) {
currentResults.add(note)
val limit = filter.limit
if (limit != null && currentResults.size > limit) {
currentResults.remove(currentResults.last())
}
update(currentResults.mapNotNull { it.event })
}
}
override fun remove(note: Note) {
if (currentResults.remove(note)) {
update(currentResults.mapNotNull { it.event })
}
}
fun init() {
currentResults = ConcurrentSkipListSet(atOnce(filter))
update(currentResults.mapNotNull { it.event })
}
}
@@ -0,0 +1,69 @@
/**
* 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.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentSkipListSet
import kotlin.collections.toList
/**
* Creates a list of notes (regular and addressable)
* that only gets updated when a new note appears.
*
* New versions of addressables do not update the list
*/
class NoteListMatchingFilter(
private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val update: (List<Note>) -> Unit,
) : Observable {
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
override fun new(
event: Event,
note: Note,
) {
if (filter.match(event)) {
if (currentResults.add(note)) {
val limit = filter.limit
if (limit != null && currentResults.size > limit) {
currentResults.remove(currentResults.last())
}
update(currentResults.toList())
}
}
}
override fun remove(note: Note) {
if (currentResults.remove(note)) {
update(currentResults.toList())
}
}
fun init() {
currentResults = ConcurrentSkipListSet(atOnce(filter))
update(currentResults.toList())
}
}
@@ -0,0 +1,33 @@
/**
* 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.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
interface Observable {
fun new(
event: Event,
note: Note,
)
fun remove(note: Note)
}