Removes Threading checks on Commons since Main Threads don't exist over there.

Remove warnings
This commit is contained in:
Vitor Pamplona
2026-06-15 10:41:40 -04:00
parent 6cc62f13d8
commit 0cfc324d87
21 changed files with 36 additions and 198 deletions
+2 -4
View File
@@ -109,7 +109,6 @@ they are shared across the GUI apps. Treat as GUI-shared, not strictly headless.
|----------------|-----|---------|
| `util` | no | KMP primitives: `KmpLock`, `WeakReference`, number/URL/codepoint helpers, list/debug helpers. **This is the only general-utility package** — there is no `utils`. |
| `keystorage` | no | Secure key storage interface (Keystore / keychain / keyring actuals). |
| `threading` | no | `checkNotInMainThread` assertion. |
| `tor` | no | Tor manager interface + settings. |
| `service` | no | Cross-cutting services: `BundledUpdate` batching (common); `service/upload` (JVM), `service/nwc`, `service/lnurl` (jvmAndroid). **Singular `service`** — there is no `services`. |
@@ -203,8 +202,7 @@ compiles: `commonMain` → `jvmAndroid` → platform-specific. See
`<feature>` if feature-scoped). Keep it CLI-safe where practical.
4. **Relay subscription / filter assembly**? → `relayClient`.
5. **A domain model or per-NIP event wrapper**? → `model` (`model/nipNN…`).
6. **A platform capability behind `expect`/`actual`** (storage, crypto,
threading)? → the matching abstraction package + actuals in platform sets.
6. **A platform capability behind `expect`/`actual`** (storage, crypto)? → the matching abstraction package + actuals in platform sets.
7. **A generic helper**? → `util`. (Resist creating a new top-level package for
one file.)
@@ -229,7 +227,7 @@ These are intentionally *documented*, not silently tolerated. Fix opportunistica
low-level EOSE bookkeeping, `relayClient` is the subscription client. Keep the
distinction in mind when adding files.
- Several **single-file feature packages** (`account`, `marmot`,
`nip53LiveActivities`, `keystorage`, `threading`) are kept as feature/abstraction
`nip53LiveActivities`, `keystorage`) are kept as feature/abstraction
namespaces expected to grow; do not fold them into `util` just for size.
- **`onchain`** (on-chain zap splitting) is `quartz`-adjacent but un-numbered;
leave readable unless a clear NIP number lands.
+3 -1
View File
@@ -37,7 +37,9 @@ kotlin {
androidResources.enable = true
withHostTest {}
withHostTest {
isReturnDefaultValues = true
}
withDeviceTest {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -1,37 +0,0 @@
/*
* 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.threading
import android.os.Looper
/**
* Android implementation of checkNotInMainThread.
* Uses Looper to detect if running on main thread.
*/
actual fun checkNotInMainThread() {
// BuildConfig check removed - commons doesn't have BuildConfig
// Enable this check in debug builds at the app level if needed
if (isMainThread()) {
throw OnMainThreadException("It should not be in the MainThread")
}
}
private fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollResponsesCache
import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread
import com.vitorpamplona.amethyst.commons.util.KmpLock
import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.commons.util.replace
@@ -723,7 +722,6 @@ open class Note(
zapPaymentRequest: Note,
zapPayment: Note?,
) {
checkNotInMainThread()
if (zapPayments[zapPaymentRequest] == null) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -110,8 +109,6 @@ class ThreadAssembler(
}
fun findThreadFor(noteId: String): ThreadInfo? {
checkNotInMainThread()
val note = cache.checkGetOrCreateNote(noteId) ?: return null
return if (note.event != null) {
@@ -1,36 +0,0 @@
/*
* 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.threading
/**
* Checks that the current code is not running on the main thread.
* Throws an exception in debug builds if called from the main thread.
*
* Platform-specific: Android uses Looper, Desktop may use different mechanism or no-op.
*/
expect fun checkNotInMainThread()
/**
* Exception thrown when code expected to run on a background thread is executed on the main thread.
*/
class OnMainThreadException(
str: String,
) : RuntimeException(str)
@@ -27,7 +27,6 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.service.BasicBundledInsert
import com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate
import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.utils.flattenToSet
@@ -87,8 +86,6 @@ class FeedContentState(
fun lastNoteCreatedAtIfFilled() = lastNoteCreatedAtWhenFullyLoaded.value
fun refreshSuspended() {
checkNotInMainThread()
isRefreshing.value = true
try {
lastFeedKey = localFilter.feedKey()
@@ -20,13 +20,21 @@
*/
package android.util
import kotlin.jvm.JvmStatic
class Log {
// @JvmStatic so the methods compile to real static methods on android.util.Log. Code compiled
// against the Android SDK (e.g. quartz's PlatformLog) emits `invokestatic Log.d(...)`; without
// @JvmStatic the companion methods would be instance methods and resolve to NoSuchMethodError
// when this fake shadows the SDK stub under :commons:testAndroidHostTest.
companion object {
@JvmStatic
fun isLoggable(
tag: String?,
msg: Int?,
): Boolean = true
@JvmStatic
fun d(
tag: String?,
msg: String?,
@@ -35,6 +43,7 @@ class Log {
return 0
}
@JvmStatic
fun i(
tag: String?,
msg: String?,
@@ -43,6 +52,7 @@ class Log {
return 0
}
@JvmStatic
fun w(
tag: String?,
msg: String?,
@@ -51,6 +61,7 @@ class Log {
return 0
}
@JvmStatic
fun w(
tag: String?,
msg: String?,
@@ -61,6 +72,7 @@ class Log {
return 0
}
@JvmStatic
fun e(
tag: String?,
msg: String?,
@@ -69,6 +81,7 @@ class Log {
return 0
}
@JvmStatic
fun e(
tag: String?,
msg: String?,
@@ -41,7 +41,6 @@ class ThumbHashTest {
val decoded = ThumbHashDecoder.decode(hashBytes)
assertNotNull(decoded)
decoded!!
assertTrue(decoded.width > 0, "decoded width should be positive")
assertTrue(decoded.height > 0, "decoded height should be positive")
@@ -74,11 +73,9 @@ class ThumbHashTest {
val viaBase64 = ThumbHashDecoder.decode(encoded)
assertNotNull(viaBase64)
viaBase64!!
val viaBytes = ThumbHashDecoder.decode(ThumbHashEncoder.encode(pixels, w, h))
assertNotNull(viaBytes)
viaBytes!!
assertEquals(viaBytes.width, viaBase64.width, "base64 path and raw path should agree on width")
assertEquals(viaBytes.height, viaBase64.height, "base64 path and raw path should agree on height")
@@ -98,7 +95,6 @@ class ThumbHashTest {
val pixels = IntArray(w * h) { 0xFF8080FF.toInt() } // opaque cornflower-ish
val decoded = ThumbHashDecoder.decode(ThumbHashEncoder.encode(pixels, w, h))
assertNotNull(decoded)
decoded!!
for (p in decoded.pixels) {
val a = (p ushr 24) and 0xff
assertEquals(255, a, "alpha should be 255 for opaque encode")
@@ -113,7 +109,6 @@ class ThumbHashTest {
val pixels = IntArray(w * h) { 0x00000000 }
val decoded = ThumbHashDecoder.decode(ThumbHashEncoder.encode(pixels, w, h))
assertNotNull(decoded)
decoded!!
// The average alpha is 0, so every decoded alpha should be at or near 0.
var maxAlpha = 0
for (p in decoded.pixels) {
@@ -134,7 +129,6 @@ class ThumbHashTest {
}
val decoded = ThumbHashDecoder.decode(ThumbHashEncoder.encode(pixels, w, h))
assertNotNull(decoded)
decoded!!
var sumR = 0
var sumG = 0
@@ -163,7 +157,7 @@ class ThumbHashTest {
val hash = ThumbHashEncoder.encode(pixels, w, h)
val ratio = ThumbHashDecoder.aspectRatio(hash)
assertNotNull(ratio)
assertTrue(ratio!! > 1f, "landscape ratio should be > 1, got $ratio")
assertTrue(ratio > 1f, "landscape ratio should be > 1, got $ratio")
}
@Test
@@ -174,7 +168,7 @@ class ThumbHashTest {
val hash = ThumbHashEncoder.encode(pixels, w, h)
val ratio = ThumbHashDecoder.aspectRatio(hash)
assertNotNull(ratio)
assertTrue(ratio!! < 1f, "portrait ratio should be < 1, got $ratio")
assertTrue(ratio < 1f, "portrait ratio should be < 1, got $ratio")
}
@Test
@@ -237,7 +231,6 @@ class ThumbHashTest {
}
val decoded = ThumbHashDecoder.decode(ThumbHashEncoder.encode(pixels, w, h))
assertNotNull(decoded)
decoded!!
assertTrue(
decoded.width in 1..32 && decoded.height in 1..32,
"expected output to fit in 32x32, got ${decoded.width}x${decoded.height}",
@@ -37,7 +37,7 @@ class NamecoinSettingsTest {
fun `parses host colon port as TLS`() {
val s = NamecoinSettings.parseServerString("example.com:50006")
assertNotNull(s)
assertEquals("example.com", s!!.host)
assertEquals("example.com", s.host)
assertEquals(50006, s.port)
assertTrue(s.useSsl)
}
@@ -46,7 +46,7 @@ class NamecoinSettingsTest {
fun `parses host colon port colon tcp as plaintext`() {
val s = NamecoinSettings.parseServerString("example.com:50001:tcp")
assertNotNull(s)
assertEquals("example.com", s!!.host)
assertEquals("example.com", s.host)
assertEquals(50001, s.port)
assertFalse(s.useSsl)
}
@@ -55,7 +55,7 @@ class NamecoinSettingsTest {
fun `parses onion address`() {
val s = NamecoinSettings.parseServerString("abc123def.onion:50001:tcp")
assertNotNull(s)
assertEquals("abc123def.onion", s!!.host)
assertEquals("abc123def.onion", s.host)
assertEquals(50001, s.port)
assertFalse(s.useSsl)
assertTrue(s.usePinnedTrustStore)
@@ -65,7 +65,7 @@ class NamecoinSettingsTest {
fun `trims whitespace`() {
val s = NamecoinSettings.parseServerString(" example.com : 50006 ")
assertNotNull(s)
assertEquals("example.com", s!!.host)
assertEquals("example.com", s.host)
assertEquals(50006, s.port)
}
@@ -128,7 +128,7 @@ class NamecoinSettingsTest {
)
val servers = settings.toElectrumxServers()
assertNotNull(servers)
assertEquals(2, servers!!.size)
assertEquals(2, servers.size)
assertEquals("server1.com", servers[0].host)
assertTrue(servers[0].useSsl)
assertEquals("server2.onion", servers[1].host)
@@ -149,7 +149,7 @@ class NamecoinSettingsTest {
)
val servers = settings.toElectrumxServers()
assertNotNull(servers)
assertEquals(1, servers!!.size)
assertEquals(1, servers.size)
assertEquals("valid.com", servers[0].host)
}
@@ -1,28 +0,0 @@
/*
* 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.threading
// Mirrors the jvmMain no-op. The iosApp module (Phase 3) can swap this for an
// `NSThread.isMainThread` check if iOS-specific main-thread restrictions need
// enforcement; today there are none.
actual fun checkNotInMainThread() {
// No-op
}
@@ -193,7 +193,7 @@ class LightningAddressResolver(
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().use { response ->
if (response.isSuccessful) {
response.body?.string()
response.body.string()
} else {
null
}
@@ -224,7 +224,7 @@ class LightningAddressResolver(
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().use { response ->
// Return body even on error — caller extracts "reason" or "message" from JSON
response.body?.string()
response.body.string()
}
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -60,7 +60,7 @@ class OkHttpLnurlEndpointResolver(
val request = Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@use null
val body = response.body?.string() ?: return@use null
val body = response.body.string()
val root = mapper.readTree(body) ?: return@use null
LnurlEndpointInfo(
nostrPubkey = root.get("nostrPubkey")?.asText()?.ifBlank { null },
@@ -76,8 +76,8 @@ open class BlossomClient(
val reason = it.headers["X-Reason"] ?: it.code.toString()
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
}
val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body")
JsonMapper.fromJson<BlossomUploadResult>(body.string())
val body = it.body.string().ifBlank { throw RuntimeException("Upload to $serverBaseUrl returned no body") }
JsonMapper.fromJson<BlossomUploadResult>(body)
}
}
@@ -108,8 +108,8 @@ open class BlossomClient(
val reason = it.headers["X-Reason"] ?: it.code.toString()
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
}
val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body")
JsonMapper.fromJson<BlossomUploadResult>(body.string())
val body = it.body.string().ifBlank { throw RuntimeException("Upload to $serverBaseUrl returned no body") }
JsonMapper.fromJson<BlossomUploadResult>(body)
}
}
}
@@ -1,32 +0,0 @@
/*
* 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.threading
/**
* Desktop JVM implementation of checkNotInMainThread.
* Currently a no-op as Desktop Compose doesn't have the same main thread restrictions as Android.
* Could be enhanced to check Swing EDT if needed.
*/
actual fun checkNotInMainThread() {
// No-op for Desktop - different threading model
// Could check for Swing EDT with: javax.swing.SwingUtilities.isEventDispatchThread()
// but Compose Desktop doesn't have the same restrictions as Android
}
@@ -43,8 +43,6 @@ class Nip54InlineMetadata {
append(it.key)
append("=")
append(UrlEncoder.encode(value))
} else {
null
}
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip01Core.metadata
import com.vitorpamplona.quartz.utils.Log
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.nullable
@@ -73,6 +74,7 @@ object BirthdayTolerantSerializer : KSerializer<Birthday?> {
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(
encoder: Encoder,
value: Birthday?,
@@ -109,8 +109,6 @@ class PoolCounts {
val filters = filters[relay]
if (!filters.isNullOrEmpty()) {
sync(CountCmd(subId, filters))
} else {
null
}
}
}
@@ -251,8 +251,6 @@ class PoolRequests {
val filters = filters[relay]
if (!filters.isNullOrEmpty()) {
sync(ReqCmd(subId, filters))
} else {
null
}
}
}
@@ -81,7 +81,7 @@ class AppDefinitionEvent(
fun includeKind(kind: Int) = tags.isTaggedKind(kind)
fun platformLinks() = tags.mapNotNull(PlatformLinkTag::parse)
fun platformLinks() = tags.platformLinks()
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
@@ -1,23 +0,0 @@
/*
* 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.nip89AppHandlers.definition
fun AppDefinitionEvent.platformLinks() = tags.platformLinks()