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.commons.services.nwc.NwcPaymentTracker
import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState 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.EventListMatchingFilter
import com.vitorpamplona.amethyst.model.observables.NoteListMatchingFilter import com.vitorpamplona.amethyst.model.observables.NoteListMatchingFilter
import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledInsert
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.amethyst.ui.note.dateFormatter
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
@@ -307,7 +307,7 @@ object LocalCache : ILocalCache, ICacheProvider {
(addressableMatches + noteMatches) (addressableMatches + noteMatches)
} }
return limitedSet.toSortedSet(DefaultFeedOrder) return limitedSet.toSortedSet(CreatedAtIdHexComparator)
} }
fun observeNotes(filter: Filter): Flow<List<Note>> = 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.AddressableNote
import com.vitorpamplona.amethyst.model.Note 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> { object CreatedAtComparator : Comparator<Note> {
override fun compare( override fun compare(
first: Note?, first: Note?,
@@ -23,10 +23,9 @@ package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.Observable 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.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter 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) * Creates a list of events (regular and addressable)
@@ -39,7 +38,7 @@ class EventListMatchingFilter(
private val update: (List<Event>) -> Unit, private val update: (List<Event>) -> Unit,
) : Observable { ) : Observable {
// Keeping this here blocks it from being cleared from memory // 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( override fun new(
event: Event, event: Event,
@@ -51,6 +50,7 @@ class EventListMatchingFilter(
if (limit != null && currentResults.size > limit) { if (limit != null && currentResults.size > limit) {
currentResults.remove(currentResults.last()) currentResults.remove(currentResults.last())
} }
update(currentResults.mapNotNull { it.event }) update(currentResults.mapNotNull { it.event })
} }
} }
@@ -62,7 +62,7 @@ class EventListMatchingFilter(
} }
fun init() { fun init() {
currentResults = cache.filter(filter) currentResults = ConcurrentSkipListSet(cache.filter(filter))
update(currentResults.mapNotNull { it.event }) 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.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.Observable 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.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter 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) * Creates a list of notes (regular and addressable)
@@ -39,7 +38,7 @@ class NoteListMatchingFilter(
private val cache: LocalCache, private val cache: LocalCache,
private val update: (List<Note>) -> Unit, private val update: (List<Note>) -> Unit,
) : Observable { ) : Observable {
var currentResults: SortedSet<Note> = sortedSetOf<Note>(DefaultFeedOrder) var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
override fun new( override fun new(
event: Event, event: Event,
@@ -64,7 +63,7 @@ class NoteListMatchingFilter(
} }
fun init() { fun init() {
currentResults = cache.filter(filter) currentResults = ConcurrentSkipListSet(cache.filter(filter))
update(currentResults.toList()) update(currentResults.toList())
} }
} }