Fixes the fact that SortedSets can have duplicated objects in parallel adds.

Moves to concurrent sets with a Comparator that checks the object's reference to remove duplications in the set when events from addressables differ
This commit is contained in:
Vitor Pamplona
2026-02-07 17:50:39 -05:00
parent 2e85d1356e
commit 452d549e43
4 changed files with 35 additions and 10 deletions
@@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList
import com.vitorpamplona.amethyst.commons.services.nwc.NwcPaymentTracker
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.model.observables.CreatedAtIdHexComparator
import com.vitorpamplona.amethyst.model.observables.EventListMatchingFilter
import com.vitorpamplona.amethyst.model.observables.NoteListMatchingFilter
import com.vitorpamplona.amethyst.service.BundledInsert
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.dateFormatter
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
@@ -307,7 +307,7 @@ object LocalCache : ILocalCache, ICacheProvider {
(addressableMatches + noteMatches)
}
return limitedSet.toSortedSet(DefaultFeedOrder)
return limitedSet.toSortedSet(CreatedAtIdHexComparator)
}
fun observeNotes(filter: Filter): Flow<List<Note>> =
@@ -23,6 +23,32 @@ package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.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?,
@@ -23,10 +23,9 @@ package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.Observable
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentSkipListSet
/**
* Creates a list of events (regular and addressable)
@@ -39,7 +38,7 @@ class EventListMatchingFilter(
private val update: (List<Event>) -> Unit,
) : Observable {
// Keeping this here blocks it from being cleared from memory
var currentResults: SortedSet<Note> = sortedSetOf<Note>(DefaultFeedOrder)
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
override fun new(
event: Event,
@@ -51,6 +50,7 @@ class EventListMatchingFilter(
if (limit != null && currentResults.size > limit) {
currentResults.remove(currentResults.last())
}
update(currentResults.mapNotNull { it.event })
}
}
@@ -62,7 +62,7 @@ class EventListMatchingFilter(
}
fun init() {
currentResults = cache.filter(filter)
currentResults = ConcurrentSkipListSet(cache.filter(filter))
update(currentResults.mapNotNull { it.event })
}
}
@@ -23,10 +23,9 @@ package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.Observable
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet
import java.util.concurrent.ConcurrentSkipListSet
/**
* Creates a list of notes (regular and addressable)
@@ -39,7 +38,7 @@ class NoteListMatchingFilter(
private val cache: LocalCache,
private val update: (List<Note>) -> Unit,
) : Observable {
var currentResults: SortedSet<Note> = sortedSetOf<Note>(DefaultFeedOrder)
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
override fun new(
event: Event,
@@ -64,7 +63,7 @@ class NoteListMatchingFilter(
}
fun init() {
currentResults = cache.filter(filter)
currentResults = ConcurrentSkipListSet(cache.filter(filter))
update(currentResults.toList())
}
}