Merge branch 'vitorpamplona:main' into kmp-completeness
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user