From 95beed16e171a45e19bb1cce421f5b51fb07ca0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 18:30:27 +0000 Subject: [PATCH] refactor: KMP WeakReference + drop synchronized(this) from commonMain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the iOS plan — clears the java.lang.ref.WeakReference blocker from commons/commonMain. Four model files migrated; one additional sync primitive replaced. - Adds expect class WeakReference in commons/commonMain/util/, with a jvmAndroid actual that typealiases to java.lang.ref.WeakReference. iOS actual will typealias to kotlin.native.ref.WeakReference when the target is added. - Channel / Chatroom / MarmotGroupChatroom: the WeakReference(null) initializer relied on platform-type nullability of java.lang.ref.WeakReference's constructor. With T : Any in the expect class, fields become nullable (WeakReference<...>? = null) and the .get() callsites become ?.get(). Behaviorally equivalent. - UserRelaysCache: same WeakReference migration, plus the synchronized(this) double-checked-locking idiom is replaced with co.touchlab.stately.concurrency.Lock + withLock (KMP). kotlin.synchronized is JVM-only; Lock comes in transitively via stately-concurrent-collections already added in the previous PR. Model-layer @Synchronized usage in Channel/Chatroom/MarmotGroupChatroom/ Note (also JVM-only) is a separate iOS blocker and a separate PR. --- .../amethyst/commons/model/Channel.kt | 14 ++++---- .../model/marmotGroups/MarmotGroupChatroom.kt | 16 ++++----- .../model/nip01Core/UserRelaysCache.kt | 7 ++-- .../commons/model/privateChats/Chatroom.kt | 12 +++---- .../amethyst/commons/util/WeakReference.kt | 34 +++++++++++++++++++ .../commons/util/WeakReference.jvmAndroid.kt | 23 +++++++++++++ 6 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.jvmAndroid.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt index eac20cda18..af328640c4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt @@ -21,13 +21,13 @@ package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import java.lang.ref.WeakReference @Stable abstract class Channel : NotesGatherer { @@ -41,10 +41,10 @@ abstract class Channel : NotesGatherer { private var relays = mapOf() - private var changesFlow: WeakReference>> = WeakReference(null) + private var changesFlow: WeakReference>>? = null fun changesFlow(): MutableSharedFlow> { - val current = changesFlow.get() + val current = changesFlow?.get() if (current != null) return current val new = MutableSharedFlow>(0, 10, BufferOverflow.DROP_OLDEST) changesFlow = WeakReference(new) @@ -107,7 +107,7 @@ abstract class Channel : NotesGatherer { addRelay(relay) } - changesFlow.get()?.tryEmit(ListChange.Addition(note)) + changesFlow?.get()?.tryEmit(ListChange.Addition(note)) flowSet?.notes?.invalidateData() } @@ -122,7 +122,7 @@ abstract class Channel : NotesGatherer { lastNote = notes.values().sortedWith(DefaultFeedOrder).firstOrNull() } - changesFlow.get()?.tryEmit(ListChange.Deletion(note)) + changesFlow?.get()?.tryEmit(ListChange.Deletion(note)) flowSet?.notes?.invalidateData() } @@ -140,7 +140,7 @@ abstract class Channel : NotesGatherer { toBeRemoved.forEach { notes.remove(it.idHex) } - changesFlow.get()?.tryEmit(ListChange.SetDeletion(toBeRemoved.toSet())) + changesFlow?.get()?.tryEmit(ListChange.SetDeletion(toBeRemoved.toSet())) flowSet?.notes?.invalidateData() @@ -156,7 +156,7 @@ abstract class Channel : NotesGatherer { hidden.forEach { notes.remove(it.idHex) } - changesFlow.get()?.tryEmit(ListChange.SetDeletion(hidden)) + changesFlow?.get()?.tryEmit(ListChange.SetDeletion(hidden)) flowSet?.notes?.invalidateData() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt index 1404783958..c5befd527b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt @@ -26,13 +26,13 @@ import com.vitorpamplona.amethyst.commons.model.Channel.Companion.DefaultFeedOrd import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NotesGatherer +import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import java.lang.ref.WeakReference /** * Represents a Marmot MLS group chat room. @@ -105,10 +105,10 @@ class MarmotGroupChatroom( fun placeholderIdHex(nostrGroupId: HexKey): HexKey = "marmot-empty-$nostrGroupId" } - private var changesFlow: WeakReference>> = WeakReference(null) + private var changesFlow: WeakReference>>? = null fun changesFlow(): MutableSharedFlow> { - val current = changesFlow.get() + val current = changesFlow?.get() if (current != null) return current val new = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) changesFlow = WeakReference(new) @@ -131,7 +131,7 @@ class MarmotGroupChatroom( } unreadCount.value += 1 - changesFlow.get()?.tryEmit(ListChange.Addition(msg)) + changesFlow?.get()?.tryEmit(ListChange.Addition(msg)) return true } return false @@ -154,7 +154,7 @@ class MarmotGroupChatroom( newestMessage = msg } - changesFlow.get()?.tryEmit(ListChange.Addition(msg)) + changesFlow?.get()?.tryEmit(ListChange.Addition(msg)) return true } return false @@ -170,7 +170,7 @@ class MarmotGroupChatroom( newestMessage = messages.maxByOrNull { it.createdAt() ?: 0L } } - changesFlow.get()?.tryEmit(ListChange.Deletion(msg)) + changesFlow?.get()?.tryEmit(ListChange.Deletion(msg)) return true } return false @@ -199,7 +199,7 @@ class MarmotGroupChatroom( val toRemove = messages.minus(toKeep) messages = toKeep - changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) + changesFlow?.get()?.tryEmit(ListChange.SetDeletion(toRemove)) return toRemove } @@ -216,7 +216,7 @@ class MarmotGroupChatroom( messages = emptySet() newestMessage = null unreadCount.value = 0 - changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) + changesFlow?.get()?.tryEmit(ListChange.SetDeletion(toRemove)) return toRemove } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserRelaysCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserRelaysCache.kt index 6717126d48..70007668bf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserRelaysCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserRelaysCache.kt @@ -21,10 +21,12 @@ package com.vitorpamplona.amethyst.commons.model.nip01Core import androidx.compose.runtime.Stable +import co.touchlab.stately.concurrency.Lock +import co.touchlab.stately.concurrency.withLock +import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import kotlinx.coroutines.flow.MutableStateFlow -import java.lang.ref.WeakReference @Stable data class RelayInfo( @@ -53,9 +55,10 @@ val DefaultOrder = class UserRelaysCache { var data: Map = mapOf() private var flow: WeakReference>? = null + private val flowLock = Lock() fun flow() = - flow?.get() ?: synchronized(this) { + flow?.get() ?: flowLock.withLock { flow?.get() ?: MutableStateFlow(Wrapper(data)).also { flow = WeakReference(it) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index 0b732db7cf..f4476777f2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NotesGatherer import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject @@ -33,7 +34,6 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import java.lang.ref.WeakReference @Stable class Chatroom : NotesGatherer { @@ -44,10 +44,10 @@ class Chatroom : NotesGatherer { var ownerSentMessage: Boolean = false var newestMessage: Note? = null - private var changesFlow: WeakReference>> = WeakReference(null) + private var changesFlow: WeakReference>>? = null fun changesFlow(): MutableSharedFlow> { - val current = changesFlow.get() + val current = changesFlow?.get() if (current != null) return current val new = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) changesFlow = WeakReference(new) @@ -82,7 +82,7 @@ class Chatroom : NotesGatherer { subjectCreatedAt = msg.createdAt() } - changesFlow.get()?.tryEmit(ListChange.Addition(msg)) + changesFlow?.get()?.tryEmit(ListChange.Addition(msg)) return true } @@ -114,7 +114,7 @@ class Chatroom : NotesGatherer { } } - changesFlow.get()?.tryEmit(ListChange.Deletion(msg)) + changesFlow?.get()?.tryEmit(ListChange.Deletion(msg)) return true } @@ -138,7 +138,7 @@ class Chatroom : NotesGatherer { val toRemove = messages.minus(toKeep) messages = toKeep - changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) + changesFlow?.get()?.tryEmit(ListChange.SetDeletion(toRemove)) return toRemove } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.kt new file mode 100644 index 0000000000..1521301189 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.kt @@ -0,0 +1,34 @@ +/* + * 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.util + +/** + * KMP-friendly weak reference. JVM/Android map to `java.lang.ref.WeakReference`; + * iOS will map to `kotlin.native.ref.WeakReference` when that target is added. + * + * The referent may be reclaimed by GC at any time after this reference is + * constructed — callers must always null-check the result of [get]. + */ +expect class WeakReference( + referent: T, +) { + fun get(): T? +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.jvmAndroid.kt new file mode 100644 index 0000000000..15b46abcaf --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/WeakReference.jvmAndroid.kt @@ -0,0 +1,23 @@ +/* + * 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.util + +actual typealias WeakReference = java.lang.ref.WeakReference