Merge branch 'vitorpamplona:main' into kmp-completeness

This commit is contained in:
KotlinGeekDev
2026-03-13 01:12:49 +01:00
committed by GitHub
40 changed files with 1376 additions and 212 deletions
@@ -31,6 +31,8 @@ fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
fun HexKey.hexToByteArrayOrNull(): ByteArray? = if (Hex.isHex(this)) Hex.decode(this) else null
fun HexKey.isValid(): Boolean = length == PUBKEY_LENGTH && Hex.isHex(this)
const val PUBKEY_LENGTH = 64
const val EVENT_ID_LENGTH = 64
@@ -59,9 +59,8 @@ class BlossomServersEvent(
fun createTagArray(servers: List<String>): Array<Array<String>> =
servers
.map {
arrayOf("server", it)
}.plusElement(AltTag.assemble(ALT))
.map { arrayOf("server", it) }
.plusElement(AltTag.assemble(ALT))
.toTypedArray()
suspend fun updateRelayList(
@@ -20,7 +20,9 @@
*/
package com.vitorpamplona.quartz.nipB7Blossom
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.Hex
/**
* Parsed representation of a BUD-10 Blossom URI.
@@ -33,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
* @param authors Hex pubkeys of blob uploaders used for BUD-03 server-list lookup (`as` params).
* @param size Blob size in bytes for verification and progress display (`sz` param).
*/
@Stable
data class BlossomUri(
val sha256: HexKey,
val extension: String,
@@ -40,6 +43,17 @@ data class BlossomUri(
val authors: List<HexKey>,
val size: Long?,
) {
fun filename(): String = "$sha256.$extension"
fun toServerUrl(): String? {
val server = servers.firstOrNull()?.removeSuffix("/") ?: return null
return if (server.startsWith("http")) {
"$server/$sha256.$extension"
} else {
"https://$server/$sha256.$extension"
}
}
/**
* Serialises back to a canonical `blossom:` URI string.
* Server URLs are percent-encoded so that `&`, `=`, and `#` inside them
@@ -65,7 +79,6 @@ data class BlossomUri(
companion object {
private const val SCHEME = "blossom:"
private val SHA256_REGEX = Regex("^[0-9a-f]{64}$")
/**
* Parses a BUD-10 URI string into a [BlossomUri], or returns `null` if the
@@ -93,7 +106,7 @@ data class BlossomUri(
extension = "bin"
}
if (!SHA256_REGEX.matches(sha256)) return null
if (sha256.length != 64 || !Hex.isHex64(sha256)) return null
// Collect repeated query parameters.
val servers = mutableListOf<String>()
@@ -21,11 +21,15 @@
package com.vitorpamplona.quartz.utils
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
@@ -219,23 +223,24 @@ suspend fun <T> anyAsync(
// Use select to wait for the first deferred to complete with 'true'
val foundTrue =
withTimeoutOrNull(timeoutMillis) {
select {
deferredResults.forEach { deferred ->
// For each deferred, if it completes and its result is 'true',
// this branch of the select expression will be chosen.
deferred.onAwait { result ->
if (result) {
true // Return true from the select expression
} else {
// If a deferred completes with false, we don't want to
// immediately end the select, so we return false, which
// lets select continue waiting for other branches.
false
val remaining = deferredResults.toMutableList()
var found = false
while (remaining.isNotEmpty() && !found) {
val (winner, value) =
select {
remaining.forEach { deferred ->
deferred.onAwait { value ->
deferred to value
}
}
}
}
remaining.remove(winner)
if (value) found = true
}
}
found
} ?: false
// Once select returns (either with true or after all deferreds complete/are cancelled),
// cancel any remaining ongoing operations.
@@ -243,5 +248,51 @@ suspend fun <T> anyAsync(
// If foundTrue is false, it means all completed with false or were cancelled.
deferredResults.forEach { it.cancel() } // Ensure all are cancelled.
return@coroutineScope foundTrue == true
return@coroutineScope foundTrue
}
/**
* Executes a mapping function asynchronously on each input in the list.
* Returns the first result as soon as the first mapping returns not null, cancelling all other ongoing operations.
*
* @param inputs A list of input objects to process.
* @param map A suspend function that takes an input object and returns a Boolean.
* @return True if any mapping function returns true, false otherwise.
*/
suspend fun <T, U> firstNotNullOrNullAsync(
inputs: List<T>,
timeoutMillis: Long = 30000,
map: suspend (T) -> U?,
): U? {
if (inputs.isEmpty()) {
return null
}
return withTimeoutOrNull(timeoutMillis) {
val channel = Channel<U>(capacity = Channel.UNLIMITED)
val jobs =
inputs.map { input ->
launch(Dispatchers.IO) {
val result = map(input)
if (result != null) {
channel.trySend(result)
}
}
}
// Close channel when all jobs complete (handles all-null case)
launch {
jobs.joinAll()
channel.close()
}
// Wait for first non-null result or null if channel closes
val result = channel.receiveCatching().getOrNull()
// Cancel all remaining jobs
jobs.forEach { it.cancel() }
result
}
}