Moves private zap decryption to async, all at once.

This commit is contained in:
Vitor Pamplona
2025-07-14 11:18:56 -04:00
parent 510583e72e
commit 909eb62db6
3 changed files with 86 additions and 12 deletions

View File

@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import coil3.util.CoilUtils.result
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
@@ -68,6 +69,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.anyAsync
import com.vitorpamplona.quartz.utils.containsAny
import com.vitorpamplona.quartz.utils.launchAndWaitAll
import com.vitorpamplona.quartz.utils.tryAndWait
@@ -496,6 +498,8 @@ open class Note(
return
}
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent?>>()
zapEvents.forEach { next ->
val zapRequest = next.key.event as LnZapRequestEvent
val zapEvent = next.value?.event as? LnZapEvent
@@ -518,21 +522,27 @@ open class Note(
}
} else {
if (account.isWriteable()) {
val result =
tryAndWait { continuation ->
zapRequest.decryptPrivateZap(account.signer) {
continuation.resume(it)
}
}
if (result?.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
onWasZappedByAuthor()
return
}
parallelDecrypt.add(Pair(zapRequest, zapEvent))
}
}
}
}
val result =
anyAsync(parallelDecrypt) { pair ->
val result =
tryAndWait { continuation ->
pair.first.decryptPrivateZap(account.signer) {
continuation.resume(it)
}
}
result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption())
}
if (result) {
onWasZappedByAuthor()
}
}
suspend fun isZappedBy(

View File

@@ -64,7 +64,9 @@ class LnZapRequestEvent(
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
@Transient private var privateZapEvent: LnZapPrivateEvent? = null
// TODO: Create a per key map with resulting options to account for rejections and avoiding reasking.
@Transient
private var privateZapEvent: LnZapPrivateEvent? = null
override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (privateZapEvent?.countMemory() ?: 0)

View File

@@ -25,9 +25,11 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.coroutines.Continuation
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
/**
@@ -153,3 +155,63 @@ suspend fun <T, K> mapNotNullAsync(
}
}
}
/**
* Executes a mapping function asynchronously on each input in the list.
* Returns true as soon as the first mapping returns true, cancelling all other ongoing operations.
*
* @param inputs A list of input objects to process.
* @param mappingFunction 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> anyAsync(
inputs: List<T>,
timeoutMillis: Long = 30000,
mappingFunction: suspend (T) -> Boolean,
): Boolean =
coroutineScope {
// Create a list to hold all our deferred results
val deferredResults =
inputs.map { input ->
async {
// Each async block will execute the mapping function.
// If this coroutine gets cancelled, CancellationException will be thrown,
// and we'll catch it to ensure it doesn't propagate further.
try {
mappingFunction(input)
} catch (e: CancellationException) {
// When cancelled, we treat it as if it didn't return true
false
}
}
}
// 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
}
}
}
}
}
// Once select returns (either with true or after all deferreds complete/are cancelled),
// cancel any remaining ongoing operations.
// If foundTrue is true, all other deferreds are implicitly cancelled by the select winning.
// 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
}