diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt index d609103c09..bf17e56df9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt @@ -29,11 +29,14 @@ import com.vitorpamplona.quartz.nip01Core.core.isValid import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri import com.vitorpamplona.quartz.utils.firstNotNullOrNullAsync +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import okhttp3.OkHttpClient @@ -57,9 +60,16 @@ class BlossomServerResolver( suspend fun findServers(uriStr: String): BlossomUriServer? { uriToUrlCache[uriStr]?.let { return it } + // Confined to Dispatchers.IO: this is reached from Compose + // `produceState`/`LaunchedEffect` (RichTextViewer, MarmotGroupIconDisplay), + // which run on the main dispatcher. The pre-suspension work here — + // BlossomUri parsing, LruCache lookups, the local-cache probe's client + // build, and the server-list flow setup — must stay off the UI thread. val result = - withTimeoutOrNull(10000) { - findServersInner(uriStr) + withContext(Dispatchers.IO) { + withTimeoutOrNull(10000) { + findServersInner(uriStr) + } } if (result != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/LocalBlossomCacheProbe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/LocalBlossomCacheProbe.kt index 1b64dbd3d2..080d944e09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/LocalBlossomCacheProbe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/LocalBlossomCacheProbe.kt @@ -22,10 +22,13 @@ package com.vitorpamplona.amethyst.service.uploads.blossom.bud10 import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import okhttp3.Request import okhttp3.coroutines.executeAsync import java.util.concurrent.TimeUnit @@ -77,33 +80,39 @@ class LocalBlossomCacheProbe( cachedAtMs = 0L } + // Confined to Dispatchers.IO because callers reach this through suspend + // resolvers invoked from Compose `LaunchedEffect`/`produceState`, which run + // on the main dispatcher: building the OkHttp client and issuing the HEAD + // must not touch the UI thread. private suspend fun probe(): Boolean = - try { - val baseClient = httpClientBuilder.okHttpClientForPreview(LOCAL_CACHE_BASE) - val client = - baseClient - .newBuilder() - .connectTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .readTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .callTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .build() + withContext(Dispatchers.IO) { + try { + val baseClient = httpClientBuilder.okHttpClientForPreview(LOCAL_CACHE_BASE) + val client = + baseClient + .newBuilder() + .connectTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .readTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .callTimeout(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build() - val request = - Request - .Builder() - .url("$LOCAL_CACHE_BASE/") - .head() - .build() + val request = + Request + .Builder() + .url("$LOCAL_CACHE_BASE/") + .head() + .build() - client.newCall(request).executeAsync().use { response -> - // Spec says HEAD / returns 2xx when available. Some implementations - // may answer 405 (method not allowed) while still being a working - // Blossom cache, so treat that as available too. - response.isSuccessful || response.code == 405 + client.newCall(request).executeAsync().use { response -> + // Spec says HEAD / returns 2xx when available. Some implementations + // may answer 405 (method not allowed) while still being a working + // Blossom cache, so treat that as available too. + response.isSuccessful || response.code == 405 + } + } catch (e: Exception) { + if (e is CancellationException) throw e + false } - } catch (e: Exception) { - if (e is CancellationException) throw e - false } private fun currentTimeMs(): Long = System.currentTimeMillis()